Close isolate connection on exit

Fix for https://github.com/simolus3/drift/issues/3449#issuecomment-3045322812

# Conflicts:
#	drift/CHANGELOG.md
#	drift/lib/native.dart
This commit is contained in:
Simon Binder
2025-11-18 20:53:23 +05:30
committed by shenlong-tanwen
parent f1b58a71c3
commit 53ef7e9f19
4 changed files with 67 additions and 19 deletions
+15
View File
@@ -1,3 +1,18 @@
## 2.28.0-dev
- Query delegates: Support nested transactions in `SupportedTransactionDelegate`.
- Fix drift server isolates leaking when their only client isolate exits without closing the database.
## 2.27.0
- Allow passing `sqlite3` callback to `NativeDatabase` to customize how SQLite
bindings are obtained.
## 2.26.1
- Add `isNotNull()` column filter for the manager APIs.
- Add optional `orderBy` parameter to more aggregate function extension.
## 2.26.0
- Add support for window functions with `WindowFunctionExpression`.
+13 -9
View File
@@ -431,15 +431,19 @@ class _NativeIsolateStartup {
static Future<void> start(_NativeIsolateStartup startup) async {
await startup.isolateSetup?.call();
final isolate = DriftIsolate.inCurrent(() {
return DatabaseConnection(NativeDatabase(
File(startup.path),
logStatements: startup.enableLogs,
cachePreparedStatements: startup.cachePreparedStatements,
enableMigrations: startup.enableMigrations,
setup: startup.setup,
));
});
final isolate = DriftIsolate.inCurrent(
() {
return DatabaseConnection(NativeDatabase(
File(startup.path),
logStatements: startup.enableLogs,
cachePreparedStatements: startup.cachePreparedStatements,
enableMigrations: startup.enableMigrations,
setup: startup.setup,
));
},
shutdownAfterLastDisconnect: true,
killIsolateWhenDone: true,
);
startup.sendServer.send(isolate);
}
+19 -10
View File
@@ -36,7 +36,13 @@ Future<(StreamChannel, bool)> connectToServer(
// If the isolate accepts the connection, it sends us a send port back which
// is then used for the rest of the communication.
final receive = ReceivePort('drift client receive');
serverConnectPort.send([receive.sendPort, serialize]);
serverConnectPort.send([
receive.sendPort,
serialize,
// The server isolate will use addOnExitListener to mark the connection as
// closed when the isolate shuts down.
Isolate.current.controlPort
]);
final controller =
StreamChannelController<Object?>(allowForeignErrors: false, sync: true);
@@ -105,16 +111,21 @@ class RunningDriftServer {
closeConnectionAfterShutdown: closeConnectionAfterShutdown,
) {
final subscription = connectPort.listen((message) {
if (message is List && message.length == 2) {
if (message
case [
final sendPort as SendPort,
final serialize as bool,
final closeOnExit as SendPort
]) {
if (onlyAcceptSingleConnection) {
connectPort.close();
}
final sendPort = message[0]! as SendPort;
final serialize = message[1]! as bool;
final clientIsolate = Isolate(closeOnExit);
final receiveForConnection =
ReceivePort('drift channel #${_counter++}');
sendPort.send(receiveForConnection.sendPort);
final sendPortForRemote = receiveForConnection.sendPort;
sendPort.send(sendPortForRemote);
final controller = StreamChannelController<Object?>(
allowForeignErrors: false, sync: true);
@@ -122,11 +133,6 @@ class RunningDriftServer {
if (message == disconnectMessage) {
// Client closed the connection
controller.local.sink.close();
if (onlyAcceptSingleConnection) {
// The only connection was closed, so shut down the server.
server.shutdown();
}
} else {
controller.local.sink.add(message);
}
@@ -137,9 +143,12 @@ class RunningDriftServer {
sendPort.send(disconnectMessage);
});
clientIsolate.addOnExitListener(sendPortForRemote,
response: disconnectMessage);
_activeConnections++;
server.serve(controller.foreign, serialize: serialize).whenComplete(() {
_activeConnections--;
clientIsolate.removeErrorListener(sendPortForRemote);
if (_activeConnections == 0 && shutDownAfterLastDisconnect) {
server.shutdown();
+20
View File
@@ -174,6 +174,26 @@ void main() {
await db.close();
}, tags: 'background_isolate');
test('can close server when client isolate exits', () async {
final shutdownCalled = Completer<void>();
final isolate = DriftIsolate.inCurrent(
() => NativeDatabase.memory(),
shutdownAfterLastDisconnect: true,
beforeShutdown: shutdownCalled.complete,
);
await Isolate.run(() async {
final connection = await isolate.connect();
// Ensure the statement works
await TodoDb(connection).customStatement('SELECT 1');
// Close the isolate without closing the database
});
// This should still shut down the server.
await shutdownCalled.future;
});
test('shutting down will close the underlying executor', () async {
final mockExecutor = MockExecutor();
final isolate =