Skip to content

Commit

Permalink
fix: Prevent creation of multiple isolates when IrisMethodChannel.ini…
Browse files Browse the repository at this point in the history
…tialize is called multiple times simultaneously (#98)

A case like:
```dart
for (int i = 0; i < 5; ++i) {
  irisMethodChannel.initilize([]);
}
```
If we do not guard the initialization only once, multiple isolates will
be created.
  • Loading branch information
littleGnAl authored Apr 19, 2024
1 parent 48146f1 commit 20779db
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 78 deletions.
57 changes: 32 additions & 25 deletions lib/src/iris_method_channel.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:async';

import 'package:async/async.dart' show AsyncMemoizer;
import 'package:flutter/foundation.dart'
show VoidCallback, debugPrint, visibleForTesting;
import 'package:flutter/services.dart' show MethodChannel;
Expand All @@ -24,6 +25,8 @@ class IrisMethodChannel {
@visibleForTesting
final ScopedObjects scopedEventHandlers = ScopedObjects();

AsyncMemoizer? _initializeCallOnce;

void _setuponDetachedFromEngineListener() {
_channel.setMethodCallHandler((call) async {
if (call.method == 'onDetachedFromEngine_fromPlatform') {
Expand All @@ -43,41 +46,44 @@ class IrisMethodChannel {
return null;
}

_setuponDetachedFromEngineListener();

final initilizationResult =
await _irisMethodChannelInternal.initilize(args);

_irisMethodChannelInternal.setIrisEventMessageListener((eventMessage) {
bool handled = false;
for (final sub in scopedEventHandlers.values) {
final scopedObjects = sub as DisposableScopedObjects;
for (final es in scopedObjects.values) {
final EventHandlerHolder eh = es as EventHandlerHolder;
// We need the event handlers with the same _EventHandlerHolderKey consume the message.
for (final e in eh.getEventHandlers()) {
if (e.handleEvent(
eventMessage.event, eventMessage.data, eventMessage.buffers)) {
handled = true;
InitilizationResult? initilizationResult;
_initializeCallOnce ??= AsyncMemoizer();
await _initializeCallOnce!.runOnce(() async {
_setuponDetachedFromEngineListener();

initilizationResult = await _irisMethodChannelInternal.initilize(args);

_irisMethodChannelInternal.setIrisEventMessageListener((eventMessage) {
bool handled = false;
for (final sub in scopedEventHandlers.values) {
final scopedObjects = sub as DisposableScopedObjects;
for (final es in scopedObjects.values) {
final EventHandlerHolder eh = es as EventHandlerHolder;
// We need the event handlers with the same _EventHandlerHolderKey consume the message.
for (final e in eh.getEventHandlers()) {
if (e.handleEvent(eventMessage.event, eventMessage.data,
eventMessage.buffers)) {
handled = true;
}
}

// Break the loop after the event handlers in the same EventHandlerHolder
// consume the message.
if (handled) {
break;
}
}

// Break the loop after the event handlers in the same EventHandlerHolder
// consume the message.
// Break the loop if there is an EventHandlerHolder consume the message.
if (handled) {
break;
}
}
});

// Break the loop if there is an EventHandlerHolder consume the message.
if (handled) {
break;
}
}
_initilized = true;
});

_initilized = true;

return initilizationResult;
}

Expand Down Expand Up @@ -117,6 +123,7 @@ class IrisMethodChannel {
_initilized = false;

await _irisMethodChannelInternal.dispose();
_initializeCallOnce = null;
}

Future<CallApiResult> registerEventHandler(
Expand Down
105 changes: 52 additions & 53 deletions lib/src/platform/io/iris_method_channel_internal_io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,8 @@ class IrisMethodChannelInternalIO implements IrisMethodChannelInternal {
late Isolate workerIsolate;
late _HotRestartFinalizer _hotRestartFinalizer;

AsyncMemoizer? _initializeCallOnce;

static Future<void> _execute(_InitilizationArgs args) async {
final SendPort mainApiCallSendPort = args.apiCallPortSendPort;
final SendPort mainEventSendPort = args.eventPortSendPort;
Expand Down Expand Up @@ -507,63 +509,60 @@ class IrisMethodChannelInternalIO implements IrisMethodChannelInternal {
return null;
}

final apiCallPort = ReceivePort();
final eventPort = ReceivePort();

_hotRestartFinalizer = _HotRestartFinalizer(_nativeBindingsProvider);

workerIsolate = await Isolate.spawn(
_execute,
_InitilizationArgs(
apiCallPort.sendPort,
eventPort.sendPort,
_hotRestartFinalizer.onExitSendPort,
_nativeBindingsProvider,
args,
),
onExit: _hotRestartFinalizer.onExitSendPort,
);

// Convert the ReceivePort into a StreamQueue to receive messages from the
// spawned isolate using a pull-based interface. Events are stored in this
// queue until they are accessed by `events.next`.
// final events = StreamQueue<dynamic>(p);
final responseQueue = StreamQueue<dynamic>(apiCallPort);

// The first message from the spawned isolate is a SendPort. This port is
// used to communicate with the spawned isolate.
// SendPort sendPort = await events.next;
final msg = await responseQueue.next;
assert(msg is InitilizationResult);
final initilizationResult = msg as InitilizationResultIO;
final requestPort = initilizationResult._apiCallPortSendPort;
_nativeHandle = initilizationResult.irisApiEngineNativeHandle;

assert(() {
_hotRestartFinalizer.debugIrisApiEngineNativeHandle =
initilizationResult.irisApiEngineNativeHandle;
_hotRestartFinalizer.debugIrisCEventHandlerNativeHandle =
initilizationResult._debugIrisCEventHandlerNativeHandle;
_hotRestartFinalizer.debugIrisEventHandlerNativeHandle =
initilizationResult._debugIrisEventHandlerNativeHandle;

return true;
}());
late InitilizationResultIO initilizationResult;
_initializeCallOnce ??= AsyncMemoizer();
await _initializeCallOnce!.runOnce(() async {
final apiCallPort = ReceivePort();
final eventPort = ReceivePort();

_hotRestartFinalizer = _HotRestartFinalizer(_nativeBindingsProvider);

workerIsolate = await Isolate.spawn(
_execute,
_InitilizationArgs(
apiCallPort.sendPort,
eventPort.sendPort,
_hotRestartFinalizer.onExitSendPort,
_nativeBindingsProvider,
args,
),
onExit: _hotRestartFinalizer.onExitSendPort,
);

final responseQueue = StreamQueue<dynamic>(apiCallPort);

final msg = await responseQueue.next;
assert(msg is InitilizationResult);
initilizationResult = msg as InitilizationResultIO;
final requestPort = initilizationResult._apiCallPortSendPort;
_nativeHandle = initilizationResult.irisApiEngineNativeHandle;

assert(() {
_hotRestartFinalizer.debugIrisApiEngineNativeHandle =
initilizationResult.irisApiEngineNativeHandle;
_hotRestartFinalizer.debugIrisCEventHandlerNativeHandle =
initilizationResult._debugIrisCEventHandlerNativeHandle;
_hotRestartFinalizer.debugIrisEventHandlerNativeHandle =
initilizationResult._debugIrisEventHandlerNativeHandle;

return true;
}());

_messenger = _Messenger(requestPort, responseQueue);

_evntSubscription = eventPort.listen((message) {
if (!_initilized) {
return;
}

_messenger = _Messenger(requestPort, responseQueue);
final eventMessage = parseMessage(message);

_evntSubscription = eventPort.listen((message) {
if (!_initilized) {
return;
}
_irisEventMessageListener?.call(eventMessage);
});

final eventMessage = parseMessage(message);

_irisEventMessageListener?.call(eventMessage);
_initilized = true;
});

_initilized = true;

return initilizationResult;
}

Expand All @@ -576,8 +575,8 @@ class IrisMethodChannelInternalIO implements IrisMethodChannelInternal {
_irisEventMessageListener = null;
_hotRestartFinalizer.dispose();
await _evntSubscription.cancel();

await _messenger.dispose();
_initializeCallOnce = null;
}

@override
Expand Down
43 changes: 43 additions & 0 deletions test/iris_method_channel_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,49 @@ void main() {
await irisMethodChannel.dispose();
});

test('only initialize once', () async {
await irisMethodChannel.initilize([]);
await irisMethodChannel.initilize([]);
await irisMethodChannel.initilize([]);

final callRecord1 = messenger.callApiRecords
.where((e) => e.methodCall.funcName == 'createApiEngine');
expect(callRecord1.length, 1);

await irisMethodChannel.dispose();
});

test('only initialize once when called simultaneously', () async {
for (int i = 0; i < 5; ++i) {
irisMethodChannel.initilize([]);
}
// Wait for the 5 times calls of `irisMethodChannel.initilize` are completed.
await Future.delayed(const Duration(milliseconds: 1000));
final callRecord1 = messenger.callApiRecords
.where((e) => e.methodCall.funcName == 'createApiEngine');
expect(callRecord1.length, 1);

await irisMethodChannel.dispose();
});

test('can re-initialize after dispose', () async {
await irisMethodChannel.initilize([]);
await irisMethodChannel.initilize([]);
await irisMethodChannel.initilize([]);
await irisMethodChannel.dispose();
final callRecord1 = messenger.callApiRecords
.where((e) => e.methodCall.funcName == 'createApiEngine');
expect(callRecord1.length, 1);

await irisMethodChannel.initilize([]);
await irisMethodChannel.initilize([]);
await irisMethodChannel.initilize([]);
final callRecord2 = messenger.callApiRecords
.where((e) => e.methodCall.funcName == 'createApiEngine');
expect(callRecord2.length, 2);
await irisMethodChannel.dispose();
});

test('invokeMethod', () async {
await irisMethodChannel.initilize([]);
final callApiResult = await irisMethodChannel
Expand Down
9 changes: 9 additions & 0 deletions test/platform/fake/fake_platform_binding_delegate_io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@ class FakeNativeBindingDelegate extends PlatformBindingsDelegateInterface {
),
);
apiCallPortSendPort.send(record);
} else {
final record = CallApiRecord(
const IrisMethodCall('createApiEngine', '{}'),
CallApiRecordApiParam(
'createApiEngine',
'{}',
),
);
apiCallPortSendPort.send(record);
}
return CreateApiEngineResult(
engineHandle,
Expand Down

0 comments on commit 20779db

Please sign in to comment.