Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Prevent creation of multiple isolates when IrisMethodChannel.initialize is called multiple times simultaneously #98

Merged
merged 3 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading