Retry on failed download. (#12293)
This commit is contained in:
parent
4c83ea8bef
commit
e1fa035b69
@ -4,39 +4,62 @@
|
|||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import '../base/context.dart';
|
||||||
import '../globals.dart';
|
import '../globals.dart';
|
||||||
import 'common.dart';
|
import 'common.dart';
|
||||||
import 'io.dart';
|
import 'io.dart';
|
||||||
|
|
||||||
const int kNetworkProblemExitCode = 50;
|
const int kNetworkProblemExitCode = 50;
|
||||||
|
|
||||||
|
typedef HttpClient HttpClientFactory();
|
||||||
|
|
||||||
/// Download a file from the given URL and return the bytes.
|
/// Download a file from the given URL and return the bytes.
|
||||||
Future<List<int>> fetchUrl(Uri url) async {
|
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 HttpClientRequest request = await httpClient.getUrl(url);
|
||||||
final HttpClientResponse response = await request.close();
|
final HttpClientResponse response = await request.close();
|
||||||
|
|
||||||
printTrace('Received response statusCode=${response.statusCode}');
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throwToolExit(
|
if (response.statusCode > 0 && response.statusCode < 500) {
|
||||||
'Download failed: $url\n'
|
throwToolExit(
|
||||||
' because (${response.statusCode}) ${response.reasonPhrase}',
|
'Download failed.\n'
|
||||||
exitCode: kNetworkProblemExitCode,
|
'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 {
|
try {
|
||||||
final BytesBuilder responseBody = new BytesBuilder(copy: false);
|
final BytesBuilder responseBody = new BytesBuilder(copy: false);
|
||||||
await for (List<int> chunk in response)
|
await for (List<int> chunk in response)
|
||||||
responseBody.add(chunk);
|
responseBody.add(chunk);
|
||||||
|
|
||||||
return responseBody.takeBytes();
|
return responseBody.takeBytes();
|
||||||
} on IOException catch (e) {
|
} on IOException catch (error) {
|
||||||
throw new ToolExit(
|
printTrace('Download error: $error');
|
||||||
'Download failed: $url\n $e',
|
return null;
|
||||||
exitCode: kNetworkProblemExitCode,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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'},
|
Platform: () => new FakePlatform()..environment = <String, String>{'FLUTTER_ALREADY_LOCKED': 'true'},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('Cache', () {
|
group('Cache', () {
|
||||||
test('should not be up to date, if some cached artifact is not', () {
|
test('should not be up to date, if some cached artifact is not', () {
|
||||||
final CachedArtifact artifact1 = new MockCachedArtifact();
|
final CachedArtifact artifact1 = new MockCachedArtifact();
|
||||||
|
@ -60,7 +60,8 @@ void _defaultInitializeContext(AppContext testContext) {
|
|||||||
..putIfAbsent(SimControl, () => new MockSimControl())
|
..putIfAbsent(SimControl, () => new MockSimControl())
|
||||||
..putIfAbsent(Usage, () => new MockUsage())
|
..putIfAbsent(Usage, () => new MockUsage())
|
||||||
..putIfAbsent(FlutterVersion, () => new MockFlutterVersion())
|
..putIfAbsent(FlutterVersion, () => new MockFlutterVersion())
|
||||||
..putIfAbsent(Clock, () => const Clock());
|
..putIfAbsent(Clock, () => const Clock())
|
||||||
|
..putIfAbsent(HttpClient, () => new MockHttpClient());
|
||||||
}
|
}
|
||||||
|
|
||||||
void testUsingContext(String description, dynamic testMethod(), {
|
void testUsingContext(String description, dynamic testMethod(), {
|
||||||
@ -244,3 +245,5 @@ class MockUsage implements Usage {
|
|||||||
class MockFlutterVersion extends Mock implements FlutterVersion {}
|
class MockFlutterVersion extends Mock implements FlutterVersion {}
|
||||||
|
|
||||||
class MockClock extends Mock implements Clock {}
|
class MockClock extends Mock implements Clock {}
|
||||||
|
|
||||||
|
class MockHttpClient extends Mock implements HttpClient {}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user