Skip to content

Commit 76ad864

Browse files
authored
Added timeout for closing devfs sync http connections. (flutter#66152)
1 parent 21ad3e7 commit 76ad864

File tree

2 files changed

+108
-9
lines changed

2 files changed

+108
-9
lines changed

packages/flutter_tools/lib/src/devfs.dart

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,11 @@ class _DevFSHttpWriter implements DevFSWriter {
239239
final String fsName;
240240
final Uri httpAddress;
241241

242-
static const int kMaxInFlight = 6;
242+
// 3 was chosen to try to limit the varience in the time it takes to execute
243+
// `await request.close()` since there is a known bug in Dart where it doesn't
244+
// always return a status code in response to a PUT request:
245+
// https://github.com/dart-lang/sdk/issues/43525.
246+
static const int kMaxInFlight = 3;
243247

244248
int _inFlight = 0;
245249
Map<Uri, DevFSContent> _outstanding;
@@ -289,13 +293,29 @@ class _DevFSHttpWriter implements DevFSWriter {
289293
_osUtils,
290294
);
291295
await request.addStream(contents);
292-
final HttpClientResponse response = await request.close();
293-
response.listen((_) {},
294-
onError: (dynamic error) {
295-
_logger.printTrace('error: $error');
296-
},
297-
cancelOnError: true,
298-
);
296+
// The contents has already been streamed, closing the request should
297+
// not take long but we are experiencing hangs with it, see #63869.
298+
//
299+
// Once the bug in Dart is solved we can remove the timeout
300+
// (https://github.com/dart-lang/sdk/issues/43525). The timeout was
301+
// chosen to be inflated based on the max observed time when running the
302+
// tests in "Google Tests".
303+
try {
304+
final HttpClientResponse response = await request.close().timeout(
305+
const Duration(milliseconds: 10000));
306+
response.listen((_) {},
307+
onError: (dynamic error) {
308+
_logger.printTrace('error: $error');
309+
},
310+
cancelOnError: true,
311+
);
312+
} on TimeoutException {
313+
request.abort();
314+
// This should throw "HttpException: Request has been aborted".
315+
await request.done;
316+
// Just to be safe we rethrow the TimeoutException.
317+
rethrow;
318+
}
299319
break;
300320
} on Exception catch (error, trace) {
301321
if (!_completer.isCompleted) {

packages/flutter_tools/test/general.shard/devfs_test.dart

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

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

78
import 'package:file/file.dart';
@@ -258,7 +259,7 @@ void main() {
258259
expect(devFS.lastCompiled, isNot(previousCompile));
259260
});
260261

261-
testWithoutContext('DevFS uses provided DevFSWriter instead of default HTTP writer', () async {
262+
testWithoutContext('DevFS uses provided DevFSWriter instead of default HTTP writer', () async {
262263
final FileSystem fileSystem = MemoryFileSystem.test();
263264
final FakeDevFSWriter writer = FakeDevFSWriter();
264265
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
@@ -332,6 +333,84 @@ void main() {
332333
Uri.parse('goodbye'): DevFSFileContent(file),
333334
}, Uri.parse('/foo/bar/devfs/')), throwsA(isA<DevFSException>()));
334335
});
336+
337+
testWithoutContext('test handles request closure hangs', () async {
338+
final FileSystem fileSystem = MemoryFileSystem.test();
339+
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
340+
requests: <VmServiceExpectation>[createDevFSRequest],
341+
);
342+
final HttpClient httpClient = MockHttpClient();
343+
final MockHttpClientRequest httpRequest = MockHttpClientRequest();
344+
when(httpRequest.headers).thenReturn(MockHttpHeaders());
345+
when(httpClient.putUrl(any)).thenAnswer((Invocation invocation) {
346+
return Future<HttpClientRequest>.value(httpRequest);
347+
});
348+
int closeCount = 0;
349+
final Completer<MockHttpClientResponse> hanger = Completer<MockHttpClientResponse>();
350+
final Completer<MockHttpClientResponse> succeeder = Completer<MockHttpClientResponse>();
351+
final List<Completer<MockHttpClientResponse>> closeCompleters =
352+
<Completer<MockHttpClientResponse>>[hanger, succeeder];
353+
succeeder.complete(MockHttpClientResponse());
354+
355+
when(httpRequest.close()).thenAnswer((Invocation invocation) {
356+
final Completer<MockHttpClientResponse> completer = closeCompleters[closeCount];
357+
closeCount += 1;
358+
return completer.future;
359+
});
360+
when(httpRequest.abort()).thenAnswer((_) {
361+
hanger.completeError(const HttpException('aborted'));
362+
});
363+
when(httpRequest.done).thenAnswer((_) {
364+
if (closeCount == 1) {
365+
return hanger.future;
366+
} else if (closeCount == 2) {
367+
return succeeder.future;
368+
} else {
369+
// This branch shouldn't happen.
370+
fail('This branch should not happen');
371+
}
372+
});
373+
374+
final BufferLogger logger = BufferLogger.test();
375+
final DevFS devFS = DevFS(
376+
fakeVmServiceHost.vmService,
377+
'test',
378+
fileSystem.currentDirectory,
379+
fileSystem: fileSystem,
380+
logger: logger,
381+
osUtils: FakeOperatingSystemUtils(),
382+
httpClient: httpClient,
383+
);
384+
385+
await devFS.create();
386+
final DateTime previousCompile = devFS.lastCompiled;
387+
388+
final MockResidentCompiler residentCompiler = MockResidentCompiler();
389+
when(residentCompiler.recompile(
390+
any,
391+
any,
392+
outputPath: anyNamed('outputPath'),
393+
packageConfig: anyNamed('packageConfig'),
394+
)).thenAnswer((Invocation invocation) async {
395+
fileSystem.file('example').createSync();
396+
return const CompilerOutput('lib/foo.txt.dill', 0, <Uri>[]);
397+
});
398+
399+
final UpdateFSReport report = await devFS.update(
400+
mainUri: Uri.parse('lib/main.dart'),
401+
generator: residentCompiler,
402+
dillOutputPath: 'lib/foo.dill',
403+
pathToReload: 'lib/foo.txt.dill',
404+
trackWidgetCreation: false,
405+
invalidatedFiles: <Uri>[],
406+
packageConfig: PackageConfig.empty,
407+
);
408+
409+
expect(report.success, true);
410+
expect(devFS.lastCompiled, isNot(previousCompile));
411+
expect(closeCount, 2);
412+
expect(logger.errorText, '');
413+
});
335414
}
336415

337416
class MockHttpClientRequest extends Mock implements HttpClientRequest {}

0 commit comments

Comments
 (0)