Skip to content

Commit 1af625b

Browse files
committed
Improve handling of uncaught errors in Isolates.
1 parent 3b79bce commit 1af625b

File tree

3 files changed

+59
-17
lines changed

3 files changed

+59
-17
lines changed

lib/src/connection_pool.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,8 @@ class SqliteConnectionPool with SqliteQueries implements SqliteConnection {
158158
if (closed || _readConnections.length >= maxReaders) {
159159
return;
160160
}
161-
bool hasCapacity = _readConnections.any((connection) => !connection.locked);
161+
bool hasCapacity = _readConnections
162+
.any((connection) => !connection.locked && !connection.closed);
162163
if (!hasCapacity) {
163164
var name = debugName == null
164165
? null

lib/src/port_channel.dart

+46-12
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ class ParentPortClient implements PortClient {
2121
final ReceivePort _receivePort = ReceivePort();
2222
final ReceivePort _errorPort = ReceivePort();
2323
bool closed = false;
24+
Object? _closeError;
25+
String? _isolateDebugName;
2426
int _nextId = 1;
2527

2628
Map<int, Completer<Object?>> handlers = HashMap();
@@ -59,33 +61,34 @@ class ParentPortClient implements PortClient {
5961
close();
6062
});
6163
_errorPort.listen((message) {
62-
var [error, stackTrace] = message;
64+
final [error, stackTraceString] = message;
65+
final stackTrace = stackTraceString == null
66+
? null
67+
: StackTrace.fromString(stackTraceString);
6368
if (!initCompleter.isCompleted) {
64-
if (stackTrace == null) {
65-
initCompleter.completeError(error);
66-
} else {
67-
initCompleter.completeError(error, StackTrace.fromString(stackTrace));
68-
}
69+
initCompleter.completeError(error, stackTrace);
6970
}
71+
_close(IsolateError(cause: error, isolateDebugName: _isolateDebugName),
72+
stackTrace);
7073
});
7174
}
7275

7376
Future<void> get ready async {
7477
await sendPortFuture;
7578
}
7679

77-
void _cancelAll(Object error) {
80+
void _cancelAll(Object error, [StackTrace? stackTrace]) {
7881
var handlers = this.handlers;
7982
this.handlers = {};
8083
for (var message in handlers.values) {
81-
message.completeError(error);
84+
message.completeError(error, stackTrace);
8285
}
8386
}
8487

8588
@override
8689
Future<T> post<T>(Object message) async {
8790
if (closed) {
88-
throw ClosedException();
91+
throw _closeError ?? const ClosedException();
8992
}
9093
var completer = Completer<T>.sync();
9194
var id = _nextId++;
@@ -98,7 +101,7 @@ class ParentPortClient implements PortClient {
98101
@override
99102
void fire(Object message) async {
100103
if (closed) {
101-
throw ClosedException();
104+
throw _closeError ?? ClosedException();
102105
}
103106
final port = sendPort ?? await sendPortFuture;
104107
port.send(_FireMessage(message));
@@ -108,17 +111,27 @@ class ParentPortClient implements PortClient {
108111
return RequestPortServer(_receivePort.sendPort);
109112
}
110113

111-
void close() async {
114+
void _close([Object? error, StackTrace? stackTrace]) {
112115
if (!closed) {
113116
closed = true;
114117

115118
_receivePort.close();
116119
_errorPort.close();
117-
_cancelAll(const ClosedException());
120+
if (error == null) {
121+
_cancelAll(const ClosedException());
122+
} else {
123+
_closeError = error;
124+
_cancelAll(error, stackTrace);
125+
}
118126
}
119127
}
120128

129+
void close() {
130+
_close();
131+
}
132+
121133
tieToIsolate(Isolate isolate) {
134+
_isolateDebugName = isolate.debugName;
122135
isolate.addErrorListener(_errorPort.sendPort);
123136
isolate.addOnExitListener(_receivePort.sendPort, response: _closeMessage);
124137
}
@@ -274,6 +287,27 @@ class _RequestMessage {
274287

275288
class ClosedException implements Exception {
276289
const ClosedException();
290+
291+
@override
292+
String toString() {
293+
return 'ClosedException';
294+
}
295+
}
296+
297+
class IsolateError extends Error {
298+
final Object cause;
299+
final String? isolateDebugName;
300+
301+
IsolateError({required this.cause, this.isolateDebugName});
302+
303+
@override
304+
String toString() {
305+
if (isolateDebugName != null) {
306+
return 'IsolateError in $isolateDebugName: $cause';
307+
} else {
308+
return 'IsolateError: $cause';
309+
}
310+
}
277311
}
278312

279313
class _PortChannelResult<T> {

test/basic_test.dart

+11-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'dart:math';
44
import 'package:sqlite3/sqlite3.dart' as sqlite;
55
import 'package:sqlite_async/mutex.dart';
66
import 'package:sqlite_async/sqlite_async.dart';
7+
import 'package:test/expect.dart';
78
import 'package:test/test.dart';
89

910
import 'util.dart';
@@ -301,8 +302,11 @@ void main() {
301302
}).catchError((error) {
302303
caughtError = error;
303304
});
304-
// This may change into a better error in the future
305-
expect(caughtError.toString(), equals("Instance of 'ClosedException'"));
305+
// The specific error message may change
306+
expect(
307+
caughtError.toString(),
308+
equals(
309+
"IsolateError in sqlite-writer: Invalid argument(s): uncaught async error"));
306310

307311
// Check that we can still continue afterwards
308312
final computed = await db.computeWithDatabase((db) async {
@@ -328,8 +332,11 @@ void main() {
328332
}).catchError((error) {
329333
caughtError = error;
330334
});
331-
// This may change into a better error in the future
332-
expect(caughtError.toString(), equals("Instance of 'ClosedException'"));
335+
// The specific message may change
336+
expect(
337+
caughtError.toString(),
338+
matches(RegExp(
339+
r'IsolateError in sqlite-\d+: Invalid argument\(s\): uncaught async error')));
333340
}
334341

335342
// Check that we can still continue afterwards

0 commit comments

Comments
 (0)