mirror of https://github.com/AMT-Cheif/drift.git
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:
parent
b7690e84d9
commit
b2bbcfae8a
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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()),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue