Skip to content

Commit

Permalink
Ping control port for unreachable servers
Browse files Browse the repository at this point in the history
Closes #3339
  • Loading branch information
simolus3 committed Nov 14, 2024
1 parent b2a0d50 commit 8401d5e
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 2 deletions.
5 changes: 5 additions & 0 deletions drift_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.2.2

- Fix infinite loop in isolate server lookups when using `shareAcrossIsolates`
across hot restarts.

## 0.2.1

- Enable serialization between background isolates where necessary.
Expand Down
54 changes: 54 additions & 0 deletions drift_flutter/lib/src/native.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ String portName(String databaseName) {
return 'drift-db/$databaseName';
}

String isolateControlPortName(String databaseName) {
return 'drift-db/$databaseName/control';
}

QueryExecutor driftDatabase({
required String name,
DriftWebOptions? web,
Expand Down Expand Up @@ -63,6 +67,21 @@ QueryExecutor driftDatabase({
// Isolate has stopped shortly after the register call. It should
// also remove the port mapping, so we can just try again in another
// iteration.
// However, it's possible for the isolate to become unreachable
// without unregistering itself (either due to a fatal error or when
// doing a hot restart). Check if the isolate is still reachable,
// and remove the mapping if it's not.
final controlPort = IsolateNameServer.lookupPortByName(
isolateControlPortName(name));
if (controlPort == null) {
continue;
}
final supposedIsolate = Isolate(controlPort);
if (!await supposedIsolate.pingWithTimeout()) {
// Yup, gone!
IsolateNameServer.removePortNameMapping(portName(name));
}
// Otherwise, the isolate is probably paused. Keep trying...
}
} else {
// No port has been registered yet! Spawn an isolate that will try to
Expand Down Expand Up @@ -106,20 +125,55 @@ void _isolateEntrypoint(_EntrypointMessage message) {
final connections = ReceivePort();
if (IsolateNameServer.registerPortWithName(
connections.sendPort, portName(message.name))) {
final controlPortName = isolateControlPortName(message.name);
final server = DriftIsolate.inCurrent(
() => NativeDatabase(File(message.path)),
port: connections,
beforeShutdown: () {
IsolateNameServer.removePortNameMapping(portName(message.name));
IsolateNameServer.removePortNameMapping(controlPortName);
},
killIsolateWhenDone: true,
shutdownAfterLastDisconnect: true,
);

message.sendResponses.send(server.connectPort);

IsolateNameServer.removePortNameMapping(controlPortName);
IsolateNameServer.registerPortWithName(
Isolate.current.controlPort, controlPortName);
} else {
// Another isolate is responsible for hosting this database, abort.
connections.close();
return;
}
}

extension PingWithTimeout on Isolate {
Future<bool> pingWithTimeout(
[Duration timeout = const Duration(milliseconds: 500)]) {
final completer = Completer<bool>();
final receive = ReceivePort();
late StreamSubscription subscription;
subscription = receive.listen((_) {
subscription.cancel();

if (!completer.isCompleted) {
completer.complete(true); // Isolate reachable!
receive.close();
}
});

ping(receive.sendPort, priority: Isolate.immediate);

Timer(timeout, () {
if (!completer.isCompleted) {
completer.complete(false); // Isolate timed out!
subscription.cancel();
receive.close();
}
});

return completer.future;
}
}
2 changes: 1 addition & 1 deletion drift_flutter/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: drift_flutter
description: Easily set up drift databases across platforms in Flutter apps.
version: 0.2.1
version: 0.2.2
repository: https://github.com/simolus3/drift
homepage: https://drift.simonbinder.eu/
issue_tracker: https://github.com/simolus3/drift/issues
Expand Down
30 changes: 29 additions & 1 deletion drift_flutter/test/native_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import 'package:drift/internal/versioned_schema.dart';
import 'package:drift/native.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:drift_flutter/src/native.dart'
show hasConfiguredSqlite, portName;
show PingWithTimeout, hasConfiguredSqlite, portName;
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sqlite3/sqlite3.dart';
Expand Down Expand Up @@ -125,6 +125,34 @@ void main() {
await raw.simpleTable.insertOne(RawValuesInsertable({}));
});
});

group('pingWithTimeout', () {
test('works with alive isolates', () async {
expect(await Isolate.current.pingWithTimeout(), true);
});

test('works with stopped isolates', () async {
final isolate = await Isolate.spawn((_) {}, '');
isolate.kill();

expect(await isolate.pingWithTimeout(), false);
});

test('works with paused isolates', () async {
final isolate = await Isolate.spawn((_) async {
while (true) {
await Future.delayed(const Duration(seconds: 10));
}
}, '');
final resume = isolate.pause();
addTearDown(() => isolate.kill());

await pumpEventQueue(); // Make sure the isolate is actually paused..
expect(await isolate.pingWithTimeout(), true); // Still reachable!

isolate.resume(resume);
});
});
}

class SimpleDatabase extends GeneratedDatabase {
Expand Down

0 comments on commit 8401d5e

Please sign in to comment.