Commit e1fa035b authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Retry on failed download. (#12293)

parent 4c83ea8b
......@@ -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;
}
}
// 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 {}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment