Skip to content

Commit a236460

Browse files
ntkmenex3
andauthored
Fix flaky shutdown (#2312)
Co-authored-by: Natalie Weizenbaum <[email protected]>
1 parent 9379d85 commit a236460

File tree

4 files changed

+102
-134
lines changed

4 files changed

+102
-134
lines changed

lib/src/embedded/compilation_dispatcher.dart

Lines changed: 27 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -51,28 +51,15 @@ final class CompilationDispatcher {
5151
/// This is used in outgoing messages.
5252
late Uint8List _compilationIdVarint;
5353

54-
/// Whether we detected a [ProtocolError] while parsing an incoming response.
55-
///
56-
/// If we have, we don't want to send the final compilation result because
57-
/// it'll just be a wrapper around the error.
58-
var _requestError = false;
59-
6054
/// Creates a [CompilationDispatcher] that receives encoded protocol buffers
6155
/// through [_mailbox] and sends them through [_sendPort].
6256
CompilationDispatcher(this._mailbox, this._sendPort);
6357

6458
/// Listens for incoming `CompileRequests` and runs their compilations.
6559
void listen() {
66-
do {
67-
Uint8List packet;
68-
try {
69-
packet = _mailbox.take();
70-
} on StateError catch (_) {
71-
break;
72-
}
73-
60+
while (true) {
7461
try {
75-
var (compilationId, messageBuffer) = parsePacket(packet);
62+
var (compilationId, messageBuffer) = parsePacket(_receive());
7663

7764
_compilationId = compilationId;
7865
_compilationIdVarint = serializeVarint(compilationId);
@@ -88,9 +75,7 @@ final class CompilationDispatcher {
8875
case InboundMessage_Message.compileRequest:
8976
var request = message.compileRequest;
9077
var response = _compile(request);
91-
if (!_requestError) {
92-
_send(OutboundMessage()..compileResponse = response);
93-
}
78+
_send(OutboundMessage()..compileResponse = response);
9479

9580
case InboundMessage_Message.versionRequest:
9681
throw paramsError("VersionRequest must have compilation ID 0.");
@@ -113,7 +98,7 @@ final class CompilationDispatcher {
11398
} catch (error, stackTrace) {
11499
_handleError(error, stackTrace);
115100
}
116-
} while (!_requestError);
101+
}
117102
}
118103

119104
OutboundMessage_CompileResponse _compile(
@@ -287,20 +272,13 @@ final class CompilationDispatcher {
287272
void sendLog(OutboundMessage_LogEvent event) =>
288273
_send(OutboundMessage()..logEvent = event);
289274

290-
/// Sends [error] to the host.
275+
/// Sends [error] to the host and exit.
291276
///
292277
/// This is used during compilation by other classes like host callable.
293-
/// Therefore it must set _requestError = true to prevent sending a CompileFailure after
294-
/// sending a ProtocolError.
295-
void sendError(ProtocolError error) {
296-
_sendError(error);
297-
_requestError = true;
278+
Never sendError(ProtocolError error) {
279+
Isolate.exit(_sendPort, _serializePacket(OutboundMessage()..error = error));
298280
}
299281

300-
/// Sends [error] to the host.
301-
void _sendError(ProtocolError error) =>
302-
_send(OutboundMessage()..error = error);
303-
304282
InboundMessage_CanonicalizeResponse sendCanonicalizeRequest(
305283
OutboundMessage_CanonicalizeRequest request) =>
306284
_sendRequest<InboundMessage_CanonicalizeResponse>(
@@ -326,19 +304,9 @@ final class CompilationDispatcher {
326304
message.id = _outboundRequestId;
327305
_send(message);
328306

329-
Uint8List packet;
330-
try {
331-
packet = _mailbox.take();
332-
} on StateError catch (_) {
333-
// Compiler is shutting down, throw without calling `_handleError` as we
334-
// don't want to report this as an actual error.
335-
_requestError = true;
336-
rethrow;
337-
}
338-
339307
try {
340308
var messageBuffer =
341-
Uint8List.sublistView(packet, _compilationIdVarint.length);
309+
Uint8List.sublistView(_receive(), _compilationIdVarint.length);
342310

343311
InboundMessage message;
344312
try {
@@ -376,21 +344,24 @@ final class CompilationDispatcher {
376344
return response;
377345
} catch (error, stackTrace) {
378346
_handleError(error, stackTrace);
379-
_requestError = true;
380-
rethrow;
381347
}
382348
}
383349

384350
/// Handles an error thrown by the dispatcher or code it dispatches to.
385351
///
386352
/// The [messageId] indicate the IDs of the message being responded to, if
387353
/// available.
388-
void _handleError(Object error, StackTrace stackTrace, {int? messageId}) {
389-
_sendError(handleError(error, stackTrace, messageId: messageId));
354+
Never _handleError(Object error, StackTrace stackTrace, {int? messageId}) {
355+
sendError(handleError(error, stackTrace, messageId: messageId));
390356
}
391357

392358
/// Sends [message] to the host with the given [wireId].
393359
void _send(OutboundMessage message) {
360+
_sendPort.send(_serializePacket(message));
361+
}
362+
363+
/// Serialize [message] to [Uint8List].
364+
Uint8List _serializePacket(OutboundMessage message) {
394365
var protobufWriter = CodedBufferWriter();
395366
message.writeToCodedBufferWriter(protobufWriter);
396367

@@ -407,6 +378,17 @@ final class CompilationDispatcher {
407378
};
408379
packet.setAll(1, _compilationIdVarint);
409380
protobufWriter.writeTo(packet, 1 + _compilationIdVarint.length);
410-
_sendPort.send(packet);
381+
return packet;
382+
}
383+
384+
/// Receive a packet from the host.
385+
Uint8List _receive() {
386+
try {
387+
return _mailbox.take();
388+
} on StateError catch (_) {
389+
// The [_mailbox] has been closed, exit the current isolate immediately
390+
// to avoid bubble the error up as [SassException] during [_sendRequest].
391+
Isolate.exit();
392+
}
411393
}
412394
}

lib/src/embedded/host_callable.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ Callable hostCallable(
5353
}
5454
} on ProtocolError catch (error, stackTrace) {
5555
dispatcher.sendError(handleError(error, stackTrace));
56-
throw error.message;
5756
}
5857
});
5958
return callable;

lib/src/embedded/isolate_dispatcher.dart

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

55
import 'dart:async';
66
import 'dart:ffi';
7+
import 'dart:io';
78
import 'dart:isolate';
89
import 'dart:typed_data';
910

@@ -27,7 +28,7 @@ class IsolateDispatcher {
2728
/// All isolates that have been spawned to dispatch to.
2829
///
2930
/// Only used for cleaning up the process when the underlying channel closes.
30-
final _allIsolates = StreamController<ReusableIsolate>();
31+
final _allIsolates = StreamController<ReusableIsolate>(sync: true);
3132

3233
/// The isolates that aren't currently running compilations
3334
final _inactiveIsolates = <ReusableIsolate>{};
@@ -43,6 +44,9 @@ class IsolateDispatcher {
4344
/// See https://github.com/sass/dart-sass/pull/2019
4445
final _isolatePool = Pool(sizeOf<IntPtr>() <= 4 ? 7 : 15);
4546

47+
/// Whether [_channel] has been closed or not.
48+
var _closed = false;
49+
4650
IsolateDispatcher(this._channel);
4751

4852
void listen() {
@@ -56,6 +60,10 @@ class IsolateDispatcher {
5660
if (compilationId != 0) {
5761
var isolate = await _activeIsolates.putIfAbsent(
5862
compilationId, () => _getIsolate(compilationId!));
63+
64+
// The shutdown may have started by the time the isolate is spawned
65+
if (_closed) return;
66+
5967
try {
6068
isolate.send(packet);
6169
return;
@@ -88,6 +96,7 @@ class IsolateDispatcher {
8896
}, onError: (Object error, StackTrace stackTrace) {
8997
_handleError(error, stackTrace);
9098
}, onDone: () {
99+
_closed = true;
91100
_allIsolates.stream.listen((isolate) => isolate.kill());
92101
});
93102
}
@@ -103,26 +112,40 @@ class IsolateDispatcher {
103112
isolate = _inactiveIsolates.first;
104113
_inactiveIsolates.remove(isolate);
105114
} else {
106-
var future = ReusableIsolate.spawn(_isolateMain);
115+
var future = ReusableIsolate.spawn(_isolateMain,
116+
onError: (Object error, StackTrace stackTrace) {
117+
_handleError(error, stackTrace);
118+
});
107119
isolate = await future;
108120
_allIsolates.add(isolate);
109121
}
110122

111-
isolate.checkOut().listen(_channel.sink.add,
112-
onError: (Object error, StackTrace stackTrace) {
113-
if (error is ProtocolError) {
114-
// Protocol errors have already been through [_handleError] in the child
115-
// isolate, so we just send them as-is and close out the underlying
116-
// channel.
117-
sendError(compilationId, error);
118-
_channel.sink.close();
119-
} else {
120-
_handleError(error, stackTrace);
123+
isolate.borrow((message) {
124+
var fullBuffer = message as Uint8List;
125+
126+
// The first byte of messages from isolates indicates whether the entire
127+
// compilation is finished (1) or if it encountered an error (2). Sending
128+
// this as part of the message buffer rather than a separate message
129+
// avoids a race condition where the host might send a new compilation
130+
// request with the same ID as one that just finished before the
131+
// [IsolateDispatcher] receives word that the isolate with that ID is
132+
// done. See sass/dart-sass#2004.
133+
var category = fullBuffer[0];
134+
var packet = Uint8List.sublistView(fullBuffer, 1);
135+
136+
switch (category) {
137+
case 0:
138+
_channel.sink.add(packet);
139+
case 1:
140+
_activeIsolates.remove(compilationId);
141+
isolate.release();
142+
_inactiveIsolates.add(isolate);
143+
resource.release();
144+
_channel.sink.add(packet);
145+
case 2:
146+
_channel.sink.add(packet);
147+
exit(exitCode);
121148
}
122-
}, onDone: () {
123-
_activeIsolates.remove(compilationId);
124-
_inactiveIsolates.add(isolate);
125-
resource.release();
126149
});
127150

128151
return isolate;

0 commit comments

Comments
 (0)