Skip to content

Commit de51ed4

Browse files
committed
Handle uncaught errors inside connection isolates.
1 parent 040571e commit de51ed4

File tree

5 files changed

+100
-0
lines changed

5 files changed

+100
-0
lines changed

lib/src/connection_pool.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ class SqliteConnectionPool with SqliteQueries implements SqliteConnection {
5858
var completer = Completer<T>();
5959

6060
var futures = _readConnections.sublist(0).map((connection) async {
61+
if (connection.closed) {
62+
_readConnections.remove(connection);
63+
}
6164
try {
6265
return await connection.readLock((ctx) async {
6366
if (haveLock) {
@@ -107,6 +110,9 @@ class SqliteConnectionPool with SqliteQueries implements SqliteConnection {
107110
if (closed) {
108111
throw AssertionError('Closed');
109112
}
113+
if (_writeConnection?.closed == true) {
114+
_writeConnection = null;
115+
}
110116
_writeConnection ??= SqliteConnectionImpl(
111117
upstreamPort: _upstreamPort,
112118
primary: false,

lib/src/sqlite_connection.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,7 @@ abstract class SqliteConnection extends SqliteWriteContext {
103103
{Duration? lockTimeout, String? debugContext});
104104

105105
Future<void> close();
106+
107+
/// Returns true if the connection is closed
108+
bool get closed;
106109
}

lib/src/sqlite_connection_impl.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ class SqliteConnectionImpl with SqliteQueries implements SqliteConnection {
4444
await _isolateClient.ready;
4545
}
4646

47+
bool get closed {
48+
return _isolateClient.closed;
49+
}
50+
4751
Future<void> _open(SqliteOpenFactory openFactory,
4852
{required bool primary,
4953
required SerializedPortClient upstreamPort}) async {
@@ -212,9 +216,20 @@ void _sqliteConnectionIsolate(_SqliteConnectionParams params) async {
212216
// running migrations, and other setup.
213217
await client.post(const InitDb());
214218
}
219+
215220
final db = await params.openFactory.open(SqliteOpenOptions(
216221
primaryConnection: params.primary, readOnly: params.readOnly));
217222

223+
runZonedGuarded(() async {
224+
await _sqliteConnectionIsolateInner(params, client, db);
225+
}, (error, stack) {
226+
db.dispose();
227+
throw error;
228+
});
229+
}
230+
231+
Future<void> _sqliteConnectionIsolateInner(_SqliteConnectionParams params,
232+
ChildPortClient client, sqlite.Database db) async {
218233
final server = params.portServer;
219234
final commandPort = ReceivePort();
220235

lib/src/sqlite_database.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ class SqliteDatabase with SqliteQueries implements SqliteConnection {
104104
await _initialized;
105105
}
106106

107+
bool get closed {
108+
return _pool.closed;
109+
}
110+
107111
void _listenForEvents() {
108112
UpdateNotification? updates;
109113

test/basic_test.dart

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,78 @@ void main() {
249249
await db.execute('BEGIN');
250250
}, throwsA((e) => e is sqlite.SqliteException));
251251
});
252+
253+
test('should handle normal errors', () async {
254+
final db = await setupDatabase(path: path);
255+
await createTables(db);
256+
Error? caughtError;
257+
final syntheticError = ArgumentError('foobar');
258+
await db.computeWithDatabase<void>((db) async {
259+
throw syntheticError;
260+
}).catchError((error) {
261+
caughtError = error;
262+
});
263+
expect(caughtError.toString(), equals(syntheticError.toString()));
264+
265+
// Check that we can still continue afterwards
266+
final computed = await db.computeWithDatabase((db) async {
267+
return 5;
268+
});
269+
expect(computed, equals(5));
270+
});
271+
272+
test('should handle uncaught errors', () async {
273+
final db = await setupDatabase(path: path);
274+
await createTables(db);
275+
Object? caughtError;
276+
await db.computeWithDatabase<void>((db) async {
277+
Future<void> asyncCompute() async {
278+
throw ArgumentError('uncaught async error');
279+
}
280+
281+
asyncCompute();
282+
}).catchError((error) {
283+
caughtError = error;
284+
});
285+
// This may change into a better error in the future
286+
expect(caughtError.toString(), equals("Instance of 'ClosedException'"));
287+
288+
// Check that we can still continue afterwards
289+
final computed = await db.computeWithDatabase((db) async {
290+
return 5;
291+
});
292+
expect(computed, equals(5));
293+
});
294+
295+
test('should handle uncaught errors in read connections', () async {
296+
final db = await setupDatabase(path: path);
297+
await createTables(db);
298+
for (var i = 0; i < 10; i++) {
299+
Object? caughtError;
300+
301+
await db.readTransaction((ctx) async {
302+
await ctx.computeWithDatabase((db) async {
303+
Future<void> asyncCompute() async {
304+
throw ArgumentError('uncaught async error');
305+
}
306+
307+
asyncCompute();
308+
});
309+
}).catchError((error) {
310+
caughtError = error;
311+
});
312+
// This may change into a better error in the future
313+
expect(caughtError.toString(), equals("Instance of 'ClosedException'"));
314+
}
315+
316+
// Check that we can still continue afterwards
317+
final computed = await db.readTransaction((ctx) async {
318+
return await ctx.computeWithDatabase((db) async {
319+
return 5;
320+
});
321+
});
322+
expect(computed, equals(5));
323+
});
252324
});
253325
}
254326

0 commit comments

Comments
 (0)