Skip to content

Commit 1203886

Browse files
authored
Isolate-based access to the SDK index (but not used yet). (#8793)
1 parent aab3c28 commit 1203886

File tree

6 files changed

+180
-30
lines changed

6 files changed

+180
-30
lines changed

app/lib/search/sdk_mem_index.dart

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'dart:async';
56
import 'dart:math';
67

78
import 'package:gcloud/service_scope.dart' as ss;
@@ -66,8 +67,18 @@ Future<SdkMemIndex?> createSdkMemIndex() async {
6667
}
6768
}
6869

70+
/// Defines the general interface for the SDK index.
71+
// ignore: one_member_abstracts
72+
abstract class SdkIndex {
73+
FutureOr<List<SdkLibraryHit>> search(
74+
String query, {
75+
int? limit,
76+
bool skipFlutter = false,
77+
});
78+
}
79+
6980
/// In-memory index for SDK library search queries.
70-
class SdkMemIndex {
81+
class SdkMemIndex implements SdkIndex {
7182
final _libraries = <String, _Library>{};
7283
final Map<String, double> _apiPageDirWeights;
7384

@@ -126,6 +137,7 @@ class SdkMemIndex {
126137
}
127138
}
128139

140+
@override
129141
List<SdkLibraryHit> search(
130142
String query, {
131143
int? limit,

app/lib/service/entrypoint/_isolate.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import 'dart:isolate';
88

99
import 'package:collection/collection.dart';
1010
import 'package:logging/logging.dart';
11+
import 'package:pub_dev/service/entrypoint/tools.dart';
12+
import 'package:pub_dev/shared/monitoring.dart';
1113
import 'package:stack_trace/stack_trace.dart';
1214

1315
import '../services.dart';
@@ -325,3 +327,31 @@ Future<void> _wrapper(List args) async {
325327
timer.cancel();
326328
}
327329
}
330+
331+
/// Run [fn] inside the isolate, using message passing from the control isolate.
332+
Future<void> runIsolateFunctions({
333+
required Object? message,
334+
required Logger logger,
335+
required Future<ReplyMessage> Function(Object payload) fn,
336+
}) async {
337+
final requestReceivePort = ReceivePort();
338+
final entryMessage = Message.fromObject(message) as EntryMessage;
339+
340+
final subs = requestReceivePort.listen((e) async {
341+
try {
342+
final msg = Message.fromObject(e) as RequestMessage;
343+
final reply = await fn(msg.payload);
344+
msg.replyPort.send(reply.encodeAsJson());
345+
} catch (e, st) {
346+
logger.pubNoticeShout(
347+
'isolate-message-error', 'Error processing message: $e', e, st);
348+
}
349+
});
350+
entryMessage.protocolSendPort.send(
351+
ReadyMessage(requestSendPort: requestReceivePort.sendPort).encodeAsJson(),
352+
);
353+
354+
await waitForProcessSignalTermination();
355+
requestReceivePort.close();
356+
await subs.cancel();
357+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:async';
6+
7+
import 'package:gcloud/service_scope.dart';
8+
import 'package:logging/logging.dart';
9+
import 'package:pub_dev/service/entrypoint/logging.dart';
10+
import 'package:pub_dev/service/services.dart';
11+
import 'package:pub_dev/shared/env_config.dart';
12+
import 'package:pub_dev/shared/logging.dart';
13+
14+
import '../../../service/entrypoint/_isolate.dart';
15+
16+
import '../../search/sdk_mem_index.dart';
17+
import '../../search/search_service.dart';
18+
19+
final _logger = Logger('sdk_search_index');
20+
21+
/// Entry point for the SDK search index isolate.
22+
Future<void> main(List<String> args, var message) async {
23+
final timer = Timer.periodic(Duration(milliseconds: 250), (_) {});
24+
25+
final ServicesWrapperFn servicesWrapperFn;
26+
if (envConfig.isRunningInAppengine) {
27+
servicesWrapperFn = withServices;
28+
setupAppEngineLogging();
29+
} else {
30+
servicesWrapperFn = (fn) => withFakeServices(fn: fn);
31+
setupDebugEnvBasedLogging();
32+
}
33+
34+
await fork(() async {
35+
await servicesWrapperFn(() async {
36+
final sdkMemIndex = await createSdkMemIndex();
37+
await runIsolateFunctions(
38+
message: message,
39+
logger: _logger,
40+
fn: (payload) async {
41+
final args = payload as List;
42+
final rs = sdkMemIndex!.search(
43+
args[0] as String,
44+
limit: args[1] as int?,
45+
skipFlutter: args[2] as bool,
46+
);
47+
return ReplyMessage.result(rs.map((e) => e.toJson()).toList());
48+
});
49+
});
50+
});
51+
52+
timer.cancel();
53+
}
54+
55+
/// Implementation of [SdkMemIndex] that uses [RequestMessage]s to send requests
56+
/// across isolate boundaries. The instance should be registered inside the
57+
/// `frontend` isolate, and it calls the `sdk-index` isolate as a delegate.
58+
class SdkIsolateIndex implements SdkIndex {
59+
final IsolateRunner _runner;
60+
SdkIsolateIndex(this._runner);
61+
62+
@override
63+
Future<List<SdkLibraryHit>> search(
64+
String query, {
65+
int? limit,
66+
bool skipFlutter = false,
67+
}) async {
68+
try {
69+
final rs = await _runner.sendRequest(
70+
[query, limit, skipFlutter],
71+
timeout: Duration(seconds: 2),
72+
);
73+
return (rs as List)
74+
.map((v) => SdkLibraryHit.fromJson(v as Map<String, dynamic>))
75+
.toList();
76+
} catch (e, st) {
77+
_logger.warning('Failed to search SDK index.', e, st);
78+
return <SdkLibraryHit>[];
79+
}
80+
}
81+
}

app/lib/service/entrypoint/search_index.dart

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import 'dart:async';
66
import 'dart:convert';
7-
import 'dart:isolate';
87

98
import 'package:gcloud/service_scope.dart';
109
import 'package:logging/logging.dart';
@@ -39,43 +38,24 @@ Future<void> main(List<String> args, var message) async {
3938
registerSdkMemIndex(await createSdkMemIndex());
4039
await indexUpdater.init();
4140

42-
final requestReceivePort = ReceivePort();
43-
final entryMessage = Message.fromObject(message) as EntryMessage;
44-
45-
final subs = requestReceivePort.listen((e) async {
46-
try {
47-
final msg = Message.fromObject(e) as RequestMessage;
48-
final payload = msg.payload;
41+
await runIsolateFunctions(
42+
message: message,
43+
logger: _logger,
44+
fn: (payload) async {
4945
if (payload is String && payload == 'info') {
5046
final info = await searchIndex.indexInfo();
51-
msg.replyPort
52-
.send(ReplyMessage.result(info.toJson()).encodeAsJson());
53-
return;
47+
return ReplyMessage.result(info.toJson());
5448
} else if (payload is String) {
5549
final q = ServiceSearchQuery.fromServiceUrl(Uri.parse(payload));
5650
final rs = await searchIndex.search(q);
57-
msg.replyPort.send(
58-
ReplyMessage.result(json.encode(rs.toJson())).encodeAsJson());
59-
return;
51+
return ReplyMessage.result(json.encode(rs.toJson()));
6052
} else {
6153
_logger.pubNoticeShout(
62-
'unknown-isolate-message', 'Unrecognized payload: $msg');
63-
msg.replyPort.send(ReplyMessage.error('Unrecognized payload: $msg')
64-
.encodeAsJson());
54+
'unknown-isolate-message', 'Unrecognized payload: $payload');
55+
return ReplyMessage.error('Unrecognized payload: $payload');
6556
}
66-
} catch (e, st) {
67-
_logger.pubNoticeShout(
68-
'isolate-message-error', 'Error processing message: $e', e, st);
69-
}
70-
});
71-
entryMessage.protocolSendPort.send(
72-
ReadyMessage(requestSendPort: requestReceivePort.sendPort)
73-
.encodeAsJson(),
57+
},
7458
);
75-
76-
await Completer().future;
77-
requestReceivePort.close();
78-
await subs.cancel();
7959
});
8060
});
8161

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:logging/logging.dart';
6+
import 'package:pub_dev/service/entrypoint/_isolate.dart';
7+
import 'package:pub_dev/service/entrypoint/sdk_isolate_index.dart';
8+
import 'package:test/test.dart';
9+
10+
final _logger = Logger('sdk_isolate_index_test');
11+
12+
void main() {
13+
group('SDK index inside an isolate', () {
14+
final indexRunner = IsolateRunner.uri(
15+
kind: 'index',
16+
logger: _logger,
17+
spawnUri: Uri.parse(
18+
'package:pub_dev/service/entrypoint/sdk_isolate_index.dart'),
19+
);
20+
21+
tearDownAll(() async {
22+
await indexRunner.close();
23+
});
24+
25+
test('start and work with index', () async {
26+
await indexRunner.start(1);
27+
28+
final sdkIndex = SdkIsolateIndex(indexRunner);
29+
30+
final rs = await sdkIndex.search('json');
31+
expect(rs.map((e) => e.toJson()).toList(), [
32+
{
33+
'sdk': 'dart',
34+
'library': 'dart:convert',
35+
'description': isNotNull,
36+
'url': 'https://api.dart.dev/stable/latest/dart-convert/',
37+
'score': isNotNull,
38+
'apiPages': isNotEmpty,
39+
},
40+
isA<Map>(), // second hit from `package:flutter_driver`
41+
]);
42+
}, timeout: Timeout(Duration(minutes: 5)));
43+
});
44+
}

app/test/shared/timer_import_test.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ void main() {
4444
// Uses timer to prevent GC compaction.
4545
'lib/service/entrypoint/search_index.dart',
4646

47+
// Uses timer to prevent GC compaction.
48+
'lib/service/entrypoint/sdk_isolate_index.dart',
49+
4750
// Uses timer to timeout GlobalLock claim acquisition.
4851
'lib/tool/neat_task/pub_dev_tasks.dart',
4952

0 commit comments

Comments
 (0)