Retry on failed download. (#12293)
This commit is contained in:
parent
4c83ea8bef
commit
e1fa035b69
@ -4,39 +4,62 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import '../base/context.dart';
|
||||
import '../globals.dart';
|
||||
import 'common.dart';
|
||||
import 'io.dart';
|
||||
|
||||
const int kNetworkProblemExitCode = 50;
|
||||
|
||||
typedef HttpClient HttpClientFactory();
|
||||
|
||||
/// Download a file from the given URL and return the bytes.
|
||||
Future<List<int>> fetchUrl(Uri url) async {
|
||||
printTrace('Downloading $url.');
|
||||
int attempts = 0;
|
||||
int duration = 1;
|
||||
while (true) {
|
||||
attempts += 1;
|
||||
final List<int> result = await _attempt(url);
|
||||
if (result != null)
|
||||
return result;
|
||||
printStatus('Download failed -- attempting retry $attempts in $duration second${ duration == 1 ? "" : "s"}...');
|
||||
await new Future<Null>.delayed(new Duration(seconds: duration));
|
||||
if (duration < 64)
|
||||
duration *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
final HttpClient httpClient = new HttpClient();
|
||||
Future<List<int>> _attempt(Uri url) async {
|
||||
printTrace('Downloading: $url');
|
||||
HttpClient httpClient;
|
||||
if (context[HttpClientFactory] != null) {
|
||||
httpClient = context[HttpClientFactory]();
|
||||
} else {
|
||||
httpClient = new HttpClient();
|
||||
}
|
||||
final HttpClientRequest request = await httpClient.getUrl(url);
|
||||
final HttpClientResponse response = await request.close();
|
||||
|
||||
printTrace('Received response statusCode=${response.statusCode}');
|
||||
if (response.statusCode != 200) {
|
||||
throwToolExit(
|
||||
'Download failed: $url\n'
|
||||
' because (${response.statusCode}) ${response.reasonPhrase}',
|
||||
exitCode: kNetworkProblemExitCode,
|
||||
);
|
||||
if (response.statusCode > 0 && response.statusCode < 500) {
|
||||
throwToolExit(
|
||||
'Download failed.\n'
|
||||
'URL: $url\n'
|
||||
'Error: ${response.statusCode} ${response.reasonPhrase}',
|
||||
exitCode: kNetworkProblemExitCode,
|
||||
);
|
||||
}
|
||||
// 5xx errors are server errors and we can try again
|
||||
printTrace('Download error: ${response.statusCode} ${response.reasonPhrase}');
|
||||
return null;
|
||||
}
|
||||
|
||||
printTrace('Received response from server, collecting bytes...');
|
||||
try {
|
||||
final BytesBuilder responseBody = new BytesBuilder(copy: false);
|
||||
await for (List<int> chunk in response)
|
||||
responseBody.add(chunk);
|
||||
|
||||
responseBody.add(chunk);
|
||||
return responseBody.takeBytes();
|
||||
} on IOException catch (e) {
|
||||
throw new ToolExit(
|
||||
'Download failed: $url\n $e',
|
||||
exitCode: kNetworkProblemExitCode,
|
||||
);
|
||||
} on IOException catch (error) {
|
||||
printTrace('Download error: $error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
115
packages/flutter_tools/test/base/net_test.dart
Normal file
115
packages/flutter_tools/test/base/net_test.dart
Normal file
@ -0,0 +1,115 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_tools/src/base/io.dart' as io;
|
||||
import 'package:flutter_tools/src/base/net.dart';
|
||||
import 'package:quiver/testing/async.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../src/context.dart';
|
||||
|
||||
void main() {
|
||||
testUsingContext('retry from 500', () async {
|
||||
String error;
|
||||
new FakeAsync().run((FakeAsync time) {
|
||||
fetchUrl(Uri.parse('http://example.invalid/')).then((List<int> value) {
|
||||
error = 'test completed unexpectedly';
|
||||
}, onError: (dynamic error) {
|
||||
error = 'test failed unexpectedly';
|
||||
});
|
||||
expect(testLogger.statusText, '');
|
||||
time.elapse(const Duration(milliseconds: 10000));
|
||||
expect(testLogger.statusText,
|
||||
'Download failed -- attempting retry 1 in 1 second...\n'
|
||||
'Download failed -- attempting retry 2 in 2 seconds...\n'
|
||||
'Download failed -- attempting retry 3 in 4 seconds...\n'
|
||||
'Download failed -- attempting retry 4 in 8 seconds...\n'
|
||||
);
|
||||
});
|
||||
expect(testLogger.errorText, isEmpty);
|
||||
expect(error, isNull);
|
||||
}, overrides: <Type, Generator>{
|
||||
HttpClientFactory: () => () => new MockHttpClient(500),
|
||||
});
|
||||
|
||||
testUsingContext('retry from network error', () async {
|
||||
String error;
|
||||
new FakeAsync().run((FakeAsync time) {
|
||||
fetchUrl(Uri.parse('http://example.invalid/')).then((List<int> value) {
|
||||
error = 'test completed unexpectedly';
|
||||
}, onError: (dynamic error) {
|
||||
error = 'test failed unexpectedly';
|
||||
});
|
||||
expect(testLogger.statusText, '');
|
||||
time.elapse(const Duration(milliseconds: 10000));
|
||||
expect(testLogger.statusText,
|
||||
'Download failed -- attempting retry 1 in 1 second...\n'
|
||||
'Download failed -- attempting retry 2 in 2 seconds...\n'
|
||||
'Download failed -- attempting retry 3 in 4 seconds...\n'
|
||||
'Download failed -- attempting retry 4 in 8 seconds...\n'
|
||||
);
|
||||
});
|
||||
expect(testLogger.errorText, isEmpty);
|
||||
expect(error, isNull);
|
||||
}, overrides: <Type, Generator>{
|
||||
HttpClientFactory: () => () => new MockHttpClient(200),
|
||||
});
|
||||
}
|
||||
|
||||
class MockHttpClient implements io.HttpClient {
|
||||
MockHttpClient(this.statusCode);
|
||||
|
||||
final int statusCode;
|
||||
|
||||
@override
|
||||
Future<io.HttpClientRequest> getUrl(Uri url) async {
|
||||
return new MockHttpClientRequest(statusCode);
|
||||
}
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) {
|
||||
throw 'io.HttpClient - $invocation';
|
||||
}
|
||||
}
|
||||
|
||||
class MockHttpClientRequest implements io.HttpClientRequest {
|
||||
MockHttpClientRequest(this.statusCode);
|
||||
|
||||
final int statusCode;
|
||||
|
||||
@override
|
||||
Future<io.HttpClientResponse> close() async {
|
||||
return new MockHttpClientResponse(statusCode);
|
||||
}
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) {
|
||||
throw 'io.HttpClientRequest - $invocation';
|
||||
}
|
||||
}
|
||||
|
||||
class MockHttpClientResponse implements io.HttpClientResponse {
|
||||
MockHttpClientResponse(this.statusCode);
|
||||
|
||||
@override
|
||||
final int statusCode;
|
||||
|
||||
@override
|
||||
String get reasonPhrase => '<reason phrase>';
|
||||
|
||||
@override
|
||||
StreamSubscription<List<int>> listen(void onData(List<int> event), {
|
||||
Function onError, void onDone(), bool cancelOnError
|
||||
}) {
|
||||
return new Stream<List<int>>.fromFuture(new Future<List<int>>.error(const io.SocketException('test')))
|
||||
.listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError);
|
||||
}
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) {
|
||||
throw 'io.HttpClientResponse - $invocation';
|
||||
}
|
||||
}
|
@ -48,6 +48,7 @@ void main() {
|
||||
Platform: () => new FakePlatform()..environment = <String, String>{'FLUTTER_ALREADY_LOCKED': 'true'},
|
||||
});
|
||||
});
|
||||
|
||||
group('Cache', () {
|
||||
test('should not be up to date, if some cached artifact is not', () {
|
||||
final CachedArtifact artifact1 = new MockCachedArtifact();
|
||||
|
@ -60,7 +60,8 @@ void _defaultInitializeContext(AppContext testContext) {
|
||||
..putIfAbsent(SimControl, () => new MockSimControl())
|
||||
..putIfAbsent(Usage, () => new MockUsage())
|
||||
..putIfAbsent(FlutterVersion, () => new MockFlutterVersion())
|
||||
..putIfAbsent(Clock, () => const Clock());
|
||||
..putIfAbsent(Clock, () => const Clock())
|
||||
..putIfAbsent(HttpClient, () => new MockHttpClient());
|
||||
}
|
||||
|
||||
void testUsingContext(String description, dynamic testMethod(), {
|
||||
@ -244,3 +245,5 @@ class MockUsage implements Usage {
|
||||
class MockFlutterVersion extends Mock implements FlutterVersion {}
|
||||
|
||||
class MockClock extends Mock implements Clock {}
|
||||
|
||||
class MockHttpClient extends Mock implements HttpClient {}
|
||||
|
Loading…
x
Reference in New Issue
Block a user