Skip to content

Commit 03718c4

Browse files
authored
Fix transaction exception handling. (#386)
1 parent 03d4221 commit 03718c4

File tree

5 files changed

+193
-94
lines changed

5 files changed

+193
-94
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 3.4.2
4+
5+
- Fix: When a transaction is rolled back, do not expose the exception on rollback, rather the original exception from the transaction.
6+
37
## 3.4.1
48

59
- Do not allow exceptions escape when closing broken connection. [#384](https://github.com/isoos/postgresql-dart/pull/384) by [pulyaevskiy](https://github.com/pulyaevskiy).

lib/src/v3/connection.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,17 @@ class PgConnectionImplementation extends _PgSessionBase implements Connection {
563563
return result;
564564
} catch (e) {
565565
if (!transaction._sessionClosed) {
566-
await transaction._sendAndMarkClosed('ROLLBACK;');
566+
try {
567+
await transaction._sendAndMarkClosed('ROLLBACK;');
568+
} catch (_) {
569+
// checking the outer exception
570+
if (e is PgException) {
571+
// Ignore exception of rollback, as the earlier exception takes precedence.
572+
} else {
573+
// Do not ignore the exception here, it may be an implementation bug we are swallowing.
574+
rethrow;
575+
}
576+
}
567577
}
568578

569579
rethrow;

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: postgres
22
description: PostgreSQL database driver. Supports statement reuse and binary protocol and connection pooling.
3-
version: 3.4.1
3+
version: 3.4.2
44
homepage: https://github.com/isoos/postgresql-dart
55
topics:
66
- sql
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import 'dart:async';
2+
3+
import 'package:postgres/postgres.dart';
4+
import 'package:test/test.dart';
5+
6+
import 'docker.dart';
7+
8+
void main() {
9+
withPostgresServer('transaction isolations', (server) {
10+
late Connection conn1;
11+
late Connection conn2;
12+
13+
setUp(() async {
14+
conn1 = await server.newConnection();
15+
conn2 = await server.newConnection();
16+
await conn1.execute('CREATE TABLE t (id INT PRIMARY KEY, counter INT)');
17+
await conn1.execute('INSERT INTO t VALUES (1, 0)');
18+
});
19+
20+
tearDown(() async {
21+
await conn1.execute('DROP TABLE t;');
22+
await conn1.close();
23+
await conn2.close();
24+
});
25+
26+
test('read committed works as expected', () async {
27+
final c1 = Completer();
28+
final c2 = Completer();
29+
final c3 = Completer();
30+
final f1 = Future.microtask(
31+
() => conn1.runTx(
32+
settings: TransactionSettings(
33+
isolationLevel: IsolationLevel.readCommitted,
34+
),
35+
(session) async {
36+
await c2.future;
37+
await session
38+
.execute('UPDATE t SET counter = counter + 1 WHERE id=1');
39+
c1.complete();
40+
// await c3.future;
41+
},
42+
),
43+
);
44+
final f2 = Future.microtask(
45+
() => conn2.runTx(
46+
settings: TransactionSettings(
47+
isolationLevel: IsolationLevel.readCommitted,
48+
),
49+
(session) async {
50+
c2.complete();
51+
await c1.future;
52+
await session
53+
.execute('UPDATE t SET counter = counter + 1 WHERE id=1');
54+
c3.complete();
55+
},
56+
),
57+
);
58+
await Future.wait([f1, f2]);
59+
final rs = await conn1.execute('SELECT * from t WHERE id=1');
60+
expect(rs.single, [1, 2]);
61+
});
62+
63+
test('forced serialization failure', () async {
64+
final c1 = Completer();
65+
final c2 = Completer();
66+
final c3 = Completer();
67+
final f1 = Future.microtask(
68+
() => conn1.runTx(
69+
settings: TransactionSettings(
70+
isolationLevel: IsolationLevel.serializable,
71+
),
72+
(session) async {
73+
await c2.future;
74+
await session
75+
.execute('UPDATE t SET counter = counter + 1 WHERE id=1');
76+
c1.complete();
77+
// await c3.future;
78+
},
79+
),
80+
);
81+
final f2 = Future.microtask(
82+
() => conn2.runTx(
83+
settings: TransactionSettings(
84+
isolationLevel: IsolationLevel.serializable,
85+
),
86+
(session) async {
87+
c2.complete();
88+
await c1.future;
89+
await session
90+
.execute('UPDATE t SET counter = counter + 1 WHERE id=1');
91+
c3.complete();
92+
},
93+
),
94+
);
95+
await expectLater(
96+
() => Future.wait([f1, f2]), throwsA(isA<ServerException>()));
97+
final rs = await conn1.execute('SELECT * from t WHERE id=1');
98+
expect(rs.single, [1, 1]);
99+
});
100+
});
101+
102+
withPostgresServer('Transaction isolation level', (server) {
103+
group('Given two rows in the database and two database connections', () {
104+
late Connection conn1;
105+
late Connection conn2;
106+
setUp(() async {
107+
conn1 = await server.newConnection();
108+
conn2 = await server.newConnection();
109+
await conn1.execute('CREATE TABLE t (id INT PRIMARY KEY, counter INT)');
110+
await conn1.execute('INSERT INTO t VALUES (1, 0)');
111+
await conn1.execute('INSERT INTO t VALUES (2, 1)');
112+
});
113+
114+
tearDown(() async {
115+
await conn1.execute('DROP TABLE t;');
116+
await conn1.close();
117+
await conn2.close();
118+
});
119+
120+
test(
121+
'when two transactions using repeatable read isolation level'
122+
'reads the row updated by the other transaction'
123+
'then one transaction throws exception ', () async {
124+
final c1 = Completer();
125+
final c2 = Completer();
126+
final f1 = Future.microtask(
127+
() => conn1.runTx(
128+
settings: TransactionSettings(
129+
isolationLevel: IsolationLevel.serializable,
130+
),
131+
(session) async {
132+
await session.execute('SELECT * from t WHERE id=1');
133+
134+
c1.complete();
135+
await c2.future;
136+
137+
await session
138+
.execute('UPDATE t SET counter = counter + 10 WHERE id=2');
139+
},
140+
),
141+
);
142+
final f2 = Future.microtask(
143+
() => conn2.runTx(
144+
settings: TransactionSettings(
145+
isolationLevel: IsolationLevel.serializable,
146+
),
147+
(session) async {
148+
await session.execute('SELECT * from t WHERE id=2');
149+
150+
await c1.future;
151+
// If we complete both transactions in parallel, we get an unexpected
152+
// exception
153+
c2.complete();
154+
155+
await session
156+
.execute('UPDATE t SET counter = counter + 20 WHERE id=1');
157+
// If we complete the first transaction after the second transaction
158+
// the correct exception is thrown
159+
// c2.complete();
160+
},
161+
),
162+
);
163+
164+
// This test throws Severity.error Session or transaction has already
165+
// finished, did you forget to await a statement?
166+
await expectLater(
167+
() => Future.wait([f1, f2]),
168+
throwsA(
169+
isA<ServerException>()
170+
.having((e) => e.severity, 'Exception severity', Severity.error)
171+
.having((e) => e.code, 'Exception code', '40001'),
172+
),
173+
);
174+
});
175+
});
176+
});
177+
}

test/transaction_test.dart

Lines changed: 0 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -704,96 +704,4 @@ void main() {
704704
);
705705
});
706706
});
707-
708-
withPostgresServer('transaction isolations', (server) {
709-
late Connection conn1;
710-
late Connection conn2;
711-
712-
setUp(() async {
713-
conn1 = await server.newConnection();
714-
conn2 = await server.newConnection();
715-
await conn1.execute('CREATE TABLE t (id INT PRIMARY KEY, counter INT)');
716-
await conn1.execute('INSERT INTO t VALUES (1, 0)');
717-
});
718-
719-
tearDown(() async {
720-
await conn1.execute('DROP TABLE t;');
721-
await conn1.close();
722-
await conn2.close();
723-
});
724-
725-
test('read committed works as expected', () async {
726-
final c1 = Completer();
727-
final c2 = Completer();
728-
final c3 = Completer();
729-
final f1 = Future.microtask(
730-
() => conn1.runTx(
731-
settings: TransactionSettings(
732-
isolationLevel: IsolationLevel.readCommitted,
733-
),
734-
(session) async {
735-
await c2.future;
736-
await session
737-
.execute('UPDATE t SET counter = counter + 1 WHERE id=1');
738-
c1.complete();
739-
// await c3.future;
740-
},
741-
),
742-
);
743-
final f2 = Future.microtask(
744-
() => conn2.runTx(
745-
settings: TransactionSettings(
746-
isolationLevel: IsolationLevel.readCommitted,
747-
),
748-
(session) async {
749-
c2.complete();
750-
await c1.future;
751-
await session
752-
.execute('UPDATE t SET counter = counter + 1 WHERE id=1');
753-
c3.complete();
754-
},
755-
),
756-
);
757-
await Future.wait([f1, f2]);
758-
final rs = await conn1.execute('SELECT * from t WHERE id=1');
759-
expect(rs.single, [1, 2]);
760-
});
761-
762-
test('forced serialization failure', () async {
763-
final c1 = Completer();
764-
final c2 = Completer();
765-
final c3 = Completer();
766-
final f1 = Future.microtask(
767-
() => conn1.runTx(
768-
settings: TransactionSettings(
769-
isolationLevel: IsolationLevel.serializable,
770-
),
771-
(session) async {
772-
await c2.future;
773-
await session
774-
.execute('UPDATE t SET counter = counter + 1 WHERE id=1');
775-
c1.complete();
776-
// await c3.future;
777-
},
778-
),
779-
);
780-
final f2 = Future.microtask(
781-
() => conn2.runTx(
782-
settings: TransactionSettings(
783-
isolationLevel: IsolationLevel.serializable,
784-
),
785-
(session) async {
786-
c2.complete();
787-
await c1.future;
788-
await session
789-
.execute('UPDATE t SET counter = counter + 1 WHERE id=1');
790-
c3.complete();
791-
},
792-
),
793-
);
794-
expectLater(() => Future.wait([f1, f2]), throwsA(isA<ServerException>()));
795-
final rs = await conn1.execute('SELECT * from t WHERE id=1');
796-
expect(rs.single, [1, 1]);
797-
});
798-
});
799707
}

0 commit comments

Comments
 (0)