Add `shutdownOnClose` to `connect()`

When only a single client connects to a drift server, the whole server
can be disposed when that client disconnects. This makes it easier to
clean up resources in the common case of having one client.

Closes #2157
This commit is contained in:
Simon Binder 2022-11-15 11:00:52 +01:00
parent b7690e84d9
commit b2bbcfae8a
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
9 changed files with 138 additions and 34 deletions

View File

@ -47,9 +47,11 @@ void main() async {
final isolate = await DriftIsolate.spawn(_backgroundConnection);
// we can now create a database connection that will use the isolate
// internally. This is NOT what's returned from _backgroundConnection, drift
// internally. This is NOT what we returned from _backgroundConnection, drift
// uses an internal proxy class for isolate communication.
final connection = await isolate.connect();
// As long as the isolate is used by only one database (it is here), we can
// use `shutdownOnClose` to dispose the isolate after closing the connection.
final connection = await isolate.connect(shutdownOnClose: true);
final db = TodoDb.connect(connection);
@ -67,7 +69,7 @@ void connectSynchronously() {
TodoDb.connect(
DatabaseConnection.delayed(Future.sync(() async {
final isolate = await DriftIsolate.spawn(_backgroundConnection);
return isolate.connect();
return isolate.connect(shutdownOnClose: true);
})),
);
// #enddocregion delayed
@ -121,7 +123,7 @@ class _IsolateStartRequest {
DatabaseConnection createDriftIsolateAndConnect() {
return DatabaseConnection.delayed(Future.sync(() async {
final isolate = await _createDriftIsolate();
return await isolate.connect();
return await isolate.connect(shutdownOnClose: true);
}));
}
// #enddocregion init_connect

View File

@ -54,6 +54,14 @@ Next, re-run the build. You can now add another constructor to the generated dat
{% include "blocks/snippet" snippets = snippets name = 'database' %}
This setup is unfortunately necessary for backwards compatibility. A
`DatabaseConnection` and the `connect` constructor make it possible to share
query streams between isolates, the default constructor can't do this. In a
future drift reelase, this option will no longer be necessary.
After adding the `connect` constructor, you can launch a drift isolate to
connect to:
## Using drift in a background isolate {#using-moor-in-a-background-isolate}
With the database class ready, let's open it on a background isolate
@ -100,10 +108,19 @@ isolate.
### Shutting down the isolate
Since multiple `DatabaseConnection`s across isolates can connect to a single `DriftIsolate`,
simply calling `Database.close` on one of them won't stop the isolate.
You can use the `DriftIsolate.shutdownAll()` for that.
It will disconnect all databases and then close the background isolate, releasing all resources.
Multiple clients can connect to a single `DriftIsolate` multiple times. So, by
default, the isolate must outlive individual connections. Simply calling
`Database.close` on one of the clients won't stop the isolate (which could
interrupt other databases).
Instead, use `DriftIsolate.shutdownAll()` to close the isolate and all clients.
This call will release all resources used by the drift isolate.
In many cases, you know that only a single client will connect to the
`DriftIsolate` (for instance because you're spawning a new `DriftIsolate` when
opening a database). In this case, you can set the `shutdownOnClose: true`
parameter on `connect()`.
With this parameter, closing the single connection will also fully dispose the
drift isolate.
## Common operation modes

View File

@ -6,6 +6,8 @@
They can be used to compare the column against a list of Dart expressions that
will be mapped through a type converter.
- Add `TableStatements.insertAll` to atomically insert multiple rows.
- Add `shutdownOnClose` to `remote()` and `DriftIsolate` connections to shutdown
a drift server or isolate after closing a database connection.
## 2.2.0

View File

@ -24,9 +24,10 @@ typedef DatabaseOpener = DatabaseConnection Function();
/// isolates, and the user facing api is exactly the same.
///
/// Please note that, while running drift in a background isolate can reduce
/// latency in foreground isolates (thus reducing UI lags), the overall
/// performance is going to be much worse as data has to be serialized and
/// deserialized to be sent over isolates.
/// lags in foreground isolates (thus removing UI jank), the overall database
/// performance will be worse. This is because result data is not available
/// directly and instead needs to be copied from the database isolate.
///
/// Also, be aware that this api is not available on the web.
///
/// See also:
@ -85,11 +86,26 @@ class DriftIsolate {
/// Connects to this [DriftIsolate] from another isolate.
///
/// All operations on the returned [DatabaseConnection] will be executed on a
/// background isolate. Setting the [isolateDebugLog] is only helpful when
/// debugging drift itself.
/// background isolate.
///
/// When [shutdownOnClose] is enabled (it defaults to `false`), the drift
/// server on the remote isolate will be shut down when this database
/// connection is closed. This option can be enabled when it is known that the
/// drift isolate will only ever serve one client.
///
/// Setting the [isolateDebugLog] is only helpful when debugging drift itself.
/// It will print messages exchanged between the two isolates.
// todo: breaking: Make synchronous in drift 2
Future<DatabaseConnection> connect({bool isolateDebugLog = false}) async {
return remote(_open(), debugLog: isolateDebugLog, serialize: serialize);
Future<DatabaseConnection> connect({
bool isolateDebugLog = false,
bool shutdownOnClose = false,
}) async {
return remote(
_open(),
debugLog: isolateDebugLog,
serialize: serialize,
shutdownOnClose: shutdownOnClose,
);
}
/// Stops the background isolate and disconnects all [DatabaseConnection]s

View File

@ -54,7 +54,7 @@ import 'package:meta/meta.dart';
import 'package:stream_channel/stream_channel.dart';
import 'drift.dart';
import 'remote.dart' as self;
import 'remote.dart' as global;
import 'src/remote/client_impl.dart';
import 'src/remote/communication.dart';
import 'src/remote/protocol.dart';
@ -82,7 +82,7 @@ abstract class DriftServer {
/// A future that completes when this server has been shut down.
///
/// This future completes after [shutdown] is called directly on this
/// instance, or if a remote client uses [self.shutdown] on a connection
/// instance, or if a remote client uses [global.shutdown] on a connection
/// handled by this server.
Future<void> get done;
@ -96,6 +96,9 @@ abstract class DriftServer {
/// [Uint8List], [String] or [List]'s thereof over the channel. Otherwise,
/// the message may be any Dart object.
///
/// After calling [serve], you can obtain a [DatabaseConnection] on the other
/// end of the [channel] by calling [remote].
///
/// __Warning__: As long as this library is marked experimental, the protocol
/// might change with every drift version. For this reason, make sure that
/// your server and clients are using the exact same version of the drift
@ -113,20 +116,30 @@ abstract class DriftServer {
/// Connects to a remote server over a two-way communication channel.
///
/// On the remote side, the corresponding [channel] must have been passed to
/// The other end of the [channel] must be attached to a drift server with
/// [DriftServer.serve] for this setup to work.
///
/// The [shutdownOnClose] parameter controls whether [shutdown] is called
/// after closing the returned database connection. By default, only this
/// connection will be closed and the server will continue to run. When enabled,
/// the server will shutdown when this connection is closed. This is useful when
/// it is known that the server will only serve a single connection.
///
/// If [serialize] is true, drift will only send [bool], [int], [double],
/// [Uint8List], [String] or [List]'s thereof over the channel. Otherwise,
/// the message may be any Dart object.
/// The value of [serialize] for [remote] should be the same value passed to
/// The value of [serialize] for [remote] must be the same value passed to
/// [DriftServer.serve].
///
/// The optional [debugLog] can be enabled to print incoming and outgoing
/// messages.
DatabaseConnection remote(StreamChannel<Object?> channel,
{bool debugLog = false, bool serialize = true}) {
final client = DriftClient(channel, debugLog, serialize);
DatabaseConnection remote(
StreamChannel<Object?> channel, {
bool debugLog = false,
bool serialize = true,
bool shutdownOnClose = false,
}) {
final client = DriftClient(channel, debugLog, serialize, shutdownOnClose);
return client.connection;
}

View File

@ -13,6 +13,7 @@ import 'protocol.dart';
/// The client part of a remote drift communication scheme.
class DriftClient {
final DriftCommunication _channel;
final bool _shutdownOnClose;
late final _RemoteStreamQueryStore _streamStore =
_RemoteStreamQueryStore(this);
@ -27,8 +28,12 @@ class DriftClient {
late QueryExecutorUser _connectedDb;
/// Starts relaying database operations over the request channel.
DriftClient(StreamChannel<Object?> channel, bool debugLog, bool serialize)
: _channel = DriftCommunication(channel,
DriftClient(
StreamChannel<Object?> channel,
bool debugLog,
bool serialize,
this._shutdownOnClose,
) : _channel = DriftCommunication(channel,
debugLog: debugLog, serialize: serialize) {
_channel.setRequestHandler(_handleRequest);
}
@ -139,8 +144,16 @@ class _RemoteQueryExecutor extends _BaseExecutor {
@override
Future<void> close() {
if (!client._channel.isClosed) {
client._channel.close();
final channel = client._channel;
if (!channel.isClosed) {
if (client._shutdownOnClose) {
return channel
.request(NoArgsRequest.terminateAll)
.whenComplete(channel.close);
} else {
channel.close();
}
}
return Future.value();

View File

@ -117,6 +117,20 @@ void main() {
await drift.shutdownAll();
}, tags: 'background_isolate');
test('kills isolate after close if desired', () async {
final spawned = ReceivePort();
final done = ReceivePort();
await Isolate.spawn(_createBackground, spawned.sendPort,
onExit: done.sendPort);
// The isolate shold eventually exit!
expect(done.first, completion(anything));
final drift = await spawned.first as DriftIsolate;
final db = TodoDb.connect(await drift.connect(shutdownOnClose: true));
await db.close();
}, tags: 'background_isolate');
test('shutting down will close the underlying executor', () async {
final mockExecutor = MockExecutor();
final isolate =

View File

@ -20,13 +20,23 @@ void main() {
DriftServer(testInMemoryDatabase(), allowRemoteShutdown: true);
server.serve(controller.foreign);
final transformed = controller.local.transformSink(
StreamSinkTransformer.fromHandlers(
handleDone: expectAsync1((inner) => inner.close()),
),
);
await shutdown(controller.local.expectedToClose);
});
await shutdown(transformed);
test('can shutdown server on close', () async {
final controller = StreamChannelController();
final server =
DriftServer(testInMemoryDatabase(), allowRemoteShutdown: true);
server.serve(controller.foreign);
final client =
remote(controller.local.expectedToClose, shutdownOnClose: true);
final db = TodoDb.connect(client);
await db.todosTable.select().get();
await db.close();
expect(server.done, completes);
});
test('Uint8Lists are mapped from and to Uint8Lists', () async {
@ -83,7 +93,9 @@ void main() {
serialize: true);
final connection = remote(
channelController.local.changeStream(_checkStreamOfSimple),
channelController.local
.changeStream(_checkStreamOfSimple)
.expectedToClose,
serialize: true);
final db = TodoDb.connect(connection);
@ -99,6 +111,8 @@ void main() {
1.2,
Uint8List(12),
]));
await db.close();
});
test('nested transactions', () async {
@ -170,3 +184,13 @@ void _checkSimple(Object? object) {
fail('Invalid message over wire: $object');
}
}
extension<T> on StreamChannel<T> {
StreamChannel<T> get expectedToClose {
return transformSink(
StreamSinkTransformer.fromHandlers(
handleDone: expectAsync1((inner) => inner.close()),
),
);
}
}

View File

@ -26,7 +26,10 @@ DatabaseConnection connect() {
_IsolateStartRequest(receiveDriftIsolate.sendPort, dbPath));
final driftIsolate = await receiveDriftIsolate.first as DriftIsolate;
return driftIsolate.connect();
// Each connect() spawns a new isolate which is only used for one
// connection, so we shutdown the isolate when the database is closed.
return driftIsolate.connect(shutdownOnClose: true);
}));
}