[frontend_server] These changes ensure that only 1 resident_frontend_server is running when multiple entities request to start the compilation server. Only 1 server is allowed to exist per server information file. Multiple resident_frontend_servers may exist if the user passes in different files each time a server is spawned.
Change-Id: I5cd0eceb391f6a289318c17e74069b63ff37126c
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/254800
Reviewed-by: Siva Annamalai <asiva@google.com>
Reviewed-by: Alexander Aprelev <aam@google.com>
Commit-Queue: Michael Richards <msrichards@google.com>
diff --git a/pkg/frontend_server/lib/src/resident_frontend_server.dart b/pkg/frontend_server/lib/src/resident_frontend_server.dart
index 6144938..2483b28 100644
--- a/pkg/frontend_server/lib/src/resident_frontend_server.dart
+++ b/pkg/frontend_server/lib/src/resident_frontend_server.dart
@@ -5,7 +5,16 @@
// @dart = 2.9
import 'dart:async';
import 'dart:convert';
-import 'dart:io' show File, InternetAddress, ServerSocket, Socket;
+import 'dart:io'
+ show
+ exit,
+ File,
+ InternetAddress,
+ Link,
+ Platform,
+ ProcessSignal,
+ ServerSocket,
+ Socket;
import 'dart:typed_data' show Uint8List;
import 'package:args/args.dart';
@@ -25,6 +34,12 @@
/// required.
const _STAT_GRANULARITY = const Duration(seconds: 1);
+const RESIDENT_SERVER_LINK_POSTFIX = '_link';
+
+/// Ensures the symbolic link is removed if ctrl-C is sent to the server.
+/// Mostly used when debugging.
+StreamSubscription<ProcessSignal> _cleanupHandler;
+
extension on DateTime {
/// Truncates by [amount].
///
@@ -280,7 +295,8 @@
if (request[_executableString] == null ||
request[_outputString] == null) {
return _encodeErrorMessage(
- 'compilation requests must include an $_executableString and an $_outputString path.');
+ 'compilation requests must include an $_executableString '
+ 'and an $_outputString path.');
}
final executablePath = request[_executableString];
final cachedDillPath = request[_outputString];
@@ -411,12 +427,28 @@
}
/// Closes the ServerSocket and removes the [serverInfoFile] that is used
-/// to access this instance of the Resident Frontend Server
+/// to access this instance of the Resident Frontend Server as well as the
+/// lock to prevent the concurrent start race.
Future<void> residentServerCleanup(
ServerSocket server, File serverInfoFile) async {
- if (serverInfoFile.existsSync()) {
- serverInfoFile.deleteSync();
+ final serverFilesystemLock =
+ Link('${serverInfoFile.path}$RESIDENT_SERVER_LINK_POSTFIX');
+ try {
+ if (_cleanupHandler != null) {
+ _cleanupHandler.cancel();
+ }
+ if (serverInfoFile.existsSync()) {
+ serverInfoFile.deleteSync();
+ }
+ } catch (_) {
+ } finally {
+ try {
+ if (serverFilesystemLock.existsSync()) {
+ serverFilesystemLock.deleteSync();
+ }
+ } catch (_) {}
}
+
await server.close();
}
@@ -437,19 +469,51 @@
InternetAddress address, int port, File serverInfoFile,
{Duration inactivityTimeout = const Duration(minutes: 30)}) async {
ServerSocket server;
+ // Create a link to the serverInfoFile to ensure that concurrent requests
+ // to start the server result in only 1 server being started. This
+ // also ensures that the serverInfoFile is only
+ // visible once the server is started and ready to receive connections.
+ // TODO https://github.com/dart-lang/sdk/issues/49647 use exclusive mode
+ // on File objects
+ final serverInfoLink =
+ Link('${serverInfoFile.path}$RESIDENT_SERVER_LINK_POSTFIX');
try {
+ try {
+ serverInfoLink.createSync(serverInfoFile.path);
+ } catch (e) {
+ // TODO: https://github.com/dart-lang/sdk/issues/49647 Using a File
+ // in exclusive mode removes the need for this check.
+ if (Platform.isWindows && e.toString().contains('errno = 1314')) {
+ throw StateError('Dart must be running in Administrator mode '
+ 'or Developer mode must be enabled when '
+ 'using the Resident Frontend Compiler.');
+ }
+ throw StateError('A server is already running.');
+ }
server = await ServerSocket.bind(address, port);
serverInfoFile
..writeAsStringSync(
'address:${server.address.address} port:${server.port}');
+ } on StateError catch (e) {
+ print('Error: $e\n');
+ return null;
} catch (e) {
+ // lock was acquired but bind or writing failed
+ try {
+ serverInfoLink.deleteSync();
+ } catch (_) {}
print('Error: $e\n');
return null;
}
+
+ _cleanupHandler = ProcessSignal.sigint.watch().listen((signal) async {
+ await residentServerCleanup(server, serverInfoFile);
+ exit(1);
+ });
var shutdownTimer =
startShutdownTimer(inactivityTimeout, server, serverInfoFile);
- print(
- 'The Resident Frontend Compiler is listening at ${server.address.address}:${server.port}');
+ print('The Resident Frontend Compiler is listening at '
+ '${server.address.address}:${server.port}');
return server.listen((client) {
client.listen((Uint8List data) async {
diff --git a/pkg/frontend_server/test/src/resident_frontend_server_test.dart b/pkg/frontend_server/test/src/resident_frontend_server_test.dart
index 0ea901e..8c44392 100644
--- a/pkg/frontend_server/test/src/resident_frontend_server_test.dart
+++ b/pkg/frontend_server/test/src/resident_frontend_server_test.dart
@@ -562,6 +562,39 @@
expect(shutdownResult['errorMessage'], contains('SocketException'));
});
+ test('concurrent startup requests', () async {
+ final serverSubscription = await residentListenAndCompile(
+ InternetAddress.loopbackIPv4,
+ 0,
+ serverInfo,
+ );
+ final startWhileAlreadyRunning = await residentListenAndCompile(
+ InternetAddress.loopbackIPv4,
+ 0,
+ serverInfo,
+ );
+
+ expect(serverSubscription, isNot(null));
+ expect(startWhileAlreadyRunning, null);
+ expect(serverInfo.existsSync(), true);
+ expect(
+ Link('${serverInfo.path}$RESIDENT_SERVER_LINK_POSTFIX').existsSync(),
+ true);
+
+ final info = serverInfo.readAsStringSync();
+ final address = InternetAddress(
+ info.substring(info.indexOf(':') + 1, info.indexOf(' ')));
+ final port = int.parse(info.substring(info.lastIndexOf(':') + 1));
+
+ final shutdownResult = await sendAndReceiveResponse(
+ address, port, ResidentFrontendServer.shutdownCommand);
+ expect(shutdownResult, equals(<String, dynamic>{"shutdown": true}));
+ expect(serverInfo.existsSync(), false);
+ expect(
+ Link('${serverInfo.path}$RESIDENT_SERVER_LINK_POSTFIX').existsSync(),
+ false);
+ });
+
test('resident server starter', () async {
final returnValue =
starter(['--resident-info-file-name=${serverInfo.path}']);