Skip to content

Commit c212668

Browse files
authored
Timeout triggers current statement cancellation (using a new connection). (#380)
1 parent 0760253 commit c212668

File tree

4 files changed

+75
-28
lines changed

4 files changed

+75
-28
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- `DatabaseInfo` tracks information about relations and oids (currently limited to `RelationMessage` caching).
1515
- **Behaviour / soft-breaking changes**:
1616
- Preparing/executing a stamement on the main connection while in a `runTx` callback will throw an exception.
17+
- Setting `timeout` will try to actively cancel the current statement using a new connection.
1718
- Deprecated `TupleDataColumn.data`, use `.value` instead (for binary protocol messages).
1819
- Deprecated some logical replication message parsing method.
1920
- Removed `@internal`-annotated methods from the public API of `ServerException` and `Severity`.

lib/src/exceptions.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,13 +190,21 @@ ServerException buildExceptionFromErrorFields(List<ErrorField> errorFields) {
190190
);
191191
}
192192

193-
PgException transformServerException(ServerException ex) {
193+
PgException transformServerException(
194+
ServerException ex, {
195+
bool timeoutTriggered = false,
196+
}) {
194197
if (ex.code == '57014' &&
195198
ex.message == 'canceling statement due to statement timeout') {
196199
return _PgTimeoutException(
197200
['${ex.code}:', ex.message, ex.trace].whereType<String>().join(' '),
198201
);
199202
}
203+
if (ex.code == '57014' && timeoutTriggered) {
204+
return _PgTimeoutException(
205+
['${ex.code}:', ex.message, ex.trace].whereType<String>().join(' '),
206+
);
207+
}
200208
return ex;
201209
}
202210

lib/src/v3/connection.dart

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -156,11 +156,9 @@ abstract class _PgSessionBase implements Session {
156156
ignoreRows,
157157
);
158158
try {
159-
await querySubscription.asFuture().optionalTimeout(timeout);
160-
return Result(
161-
rows: items,
162-
affectedRows: await querySubscription.affectedRows,
163-
schema: await querySubscription.schema,
159+
return await querySubscription._waitForResult(
160+
items: items,
161+
timeout: timeout,
164162
);
165163
} finally {
166164
await querySubscription.cancel();
@@ -260,6 +258,7 @@ class PgConnectionImplementation extends _PgSessionBase implements Connection {
260258
),
261259
async.StreamSinkTransformer.fromHandlers(handleData: (msg, sink) {
262260
print('[$hash][out] $msg');
261+
print('[out] $msg');
263262
sink.add(msg);
264263
}),
265264
));
@@ -655,16 +654,13 @@ class _PreparedStatement extends Statement {
655654
final items = <ResultRow>[];
656655
final subscription = bind(parameters).listen(items.add);
657656
try {
658-
await subscription.asFuture().optionalTimeout(timeout);
657+
return await (subscription as _PgResultStreamSubscription)._waitForResult(
658+
items: items,
659+
timeout: timeout,
660+
);
659661
} finally {
660662
await subscription.cancel();
661663
}
662-
663-
return Result(
664-
rows: items,
665-
affectedRows: await subscription.affectedRows,
666-
schema: await subscription.schema,
667-
);
668664
}
669665

670666
@override
@@ -892,6 +888,34 @@ class _PgResultStreamSubscription
892888
}
893889
}
894890

891+
Future<Result> _waitForResult({
892+
required List<ResultRow> items,
893+
required Duration? timeout,
894+
}) async {
895+
bool timeoutTriggered = false;
896+
final cancelTimer = timeout == null
897+
? null
898+
: Timer(timeout, () async {
899+
timeoutTriggered = true;
900+
await connection.cancelPendingStatement();
901+
});
902+
try {
903+
await asFuture();
904+
return Result(
905+
rows: items,
906+
affectedRows: await affectedRows,
907+
schema: await schema,
908+
);
909+
} on ServerException catch (e) {
910+
if (timeoutTriggered) {
911+
throw transformServerException(e, timeoutTriggered: timeoutTriggered);
912+
}
913+
rethrow;
914+
} finally {
915+
cancelTimer?.cancel();
916+
}
917+
}
918+
895919
// Forwarding subscription interface to regular stream subscription from
896920
// controller
897921

test/timeout_test.dart

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -103,19 +103,33 @@ void main() {
103103

104104
// Note: to fix this, we may consider cancelling the currently running statements:
105105
// https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-CANCELING-REQUESTS
106-
// withPostgresServer('timeout race conditions', (server) {
107-
// test('two transactions for update', () async {
108-
// final c1 = await server.newConnection();
109-
// final c2 = await server.newConnection();
110-
// await c1.execute('CREATE TABLE t (id INT PRIMARY KEY);');
111-
// await c1.execute('INSERT INTO t (id) values (1);');
112-
// await c1.execute('BEGIN');
113-
// await c1.execute('SELECT * FROM t WHERE id=1 FOR UPDATE');
114-
// await c2.execute('BEGIN');
115-
// await c2.execute('SELECT * FROM t WHERE id=1 FOR UPDATE',
116-
// timeout: Duration(seconds: 1));
117-
// await c1.execute('ROLLBACK');
118-
// await c2.execute('ROLLBACK');
119-
// });
120-
// });
106+
withPostgresServer('timeout race conditions', (server) {
107+
setUp(() async {
108+
final c1 = await server.newConnection();
109+
await c1.execute('CREATE TABLE t (id INT PRIMARY KEY);');
110+
await c1.execute('INSERT INTO t (id) values (1);');
111+
});
112+
113+
test('two transactions for update', () async {
114+
for (final qm in QueryMode.values) {
115+
final c1 = await server.newConnection();
116+
final c2 = await server.newConnection(queryMode: qm);
117+
await c1.execute('BEGIN');
118+
await c1.execute('SELECT * FROM t WHERE id=1 FOR UPDATE');
119+
await c2.execute('BEGIN');
120+
try {
121+
await c2.execute('SELECT * FROM t WHERE id=1 FOR UPDATE',
122+
timeout: Duration(seconds: 1));
123+
fail('unreachable');
124+
} on TimeoutException catch (_) {
125+
// ignore
126+
}
127+
await c1.execute('ROLLBACK');
128+
await c2.execute('ROLLBACK');
129+
130+
await c1.execute('SELECT 1');
131+
await c2.execute('SELECT 1');
132+
}
133+
});
134+
});
121135
}

0 commit comments

Comments
 (0)