Improve Gradle retry logic (#96554)

This commit is contained in:
Emmanuel Garcia 2022-02-18 13:54:41 -08:00 committed by GitHub
parent f9921ebcda
commit c8266d34f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 71 additions and 70 deletions

View File

@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:math';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:process/process.dart'; import 'package:process/process.dart';
@ -106,6 +108,9 @@ Iterable<String> _apkFilesFor(AndroidBuildInfo androidBuildInfo) {
return <String>['app$flavorString-$buildType.apk']; return <String>['app$flavorString-$buildType.apk'];
} }
// The maximum time to wait before the tool retries a Gradle build.
const Duration kMaxRetryTime = Duration(seconds: 10);
/// An implementation of the [AndroidBuilder] that delegates to gradle. /// An implementation of the [AndroidBuilder] that delegates to gradle.
class AndroidGradleBuilder implements AndroidBuilder { class AndroidGradleBuilder implements AndroidBuilder {
AndroidGradleBuilder({ AndroidGradleBuilder({
@ -212,7 +217,7 @@ class AndroidGradleBuilder implements AndroidBuilder {
/// * [target] is the target dart entry point. Typically, `lib/main.dart`. /// * [target] is the target dart entry point. Typically, `lib/main.dart`.
/// * If [isBuildingBundle] is `true`, then the output artifact is an `*.aab`, /// * If [isBuildingBundle] is `true`, then the output artifact is an `*.aab`,
/// otherwise the output artifact is an `*.apk`. /// otherwise the output artifact is an `*.apk`.
/// * [retries] is the max number of build retries in case one of the [GradleHandledError] handler /// * [maxRetries] If not `null`, this is the max number of build retries in case a retry is triggered.
Future<void> buildGradleApp({ Future<void> buildGradleApp({
required FlutterProject project, required FlutterProject project,
required AndroidBuildInfo androidBuildInfo, required AndroidBuildInfo androidBuildInfo,
@ -221,7 +226,8 @@ class AndroidGradleBuilder implements AndroidBuilder {
required List<GradleHandledError> localGradleErrors, required List<GradleHandledError> localGradleErrors,
bool validateDeferredComponents = true, bool validateDeferredComponents = true,
bool deferredComponentsEnabled = false, bool deferredComponentsEnabled = false,
int retries = 1, int retry = 0,
@visibleForTesting int? maxRetries,
}) async { }) async {
assert(project != null); assert(project != null);
assert(androidBuildInfo != null); assert(androidBuildInfo != null);
@ -401,7 +407,7 @@ class AndroidGradleBuilder implements AndroidBuilder {
'Gradle task $assembleTask failed with exit code $exitCode', 'Gradle task $assembleTask failed with exit code $exitCode',
exitCode: exitCode, exitCode: exitCode,
); );
} else { }
final GradleBuildStatus status = await detectedGradleError!.handler( final GradleBuildStatus status = await detectedGradleError!.handler(
line: detectedGradleErrorLine!, line: detectedGradleErrorLine!,
project: project, project: project,
@ -409,22 +415,29 @@ class AndroidGradleBuilder implements AndroidBuilder {
multidexEnabled: androidBuildInfo.multidexEnabled, multidexEnabled: androidBuildInfo.multidexEnabled,
); );
if (retries >= 1) { if (maxRetries == null || retry < maxRetries) {
final String successEventLabel = 'gradle-${detectedGradleError!.eventLabel}-success';
switch (status) { switch (status) {
case GradleBuildStatus.retry: case GradleBuildStatus.retry:
// Use binary exponential backoff before retriggering the build.
// The expected wait times are: 100ms, 200ms, 400ms, and so on...
final int waitTime = min(pow(2, retry).toInt() * 100, kMaxRetryTime.inMicroseconds);
retry += 1;
_logger.printStatus('Retrying Gradle Build: #$retry, wait time: ${waitTime}ms');
await Future<void>.delayed(Duration(milliseconds: waitTime));
await buildGradleApp( await buildGradleApp(
project: project, project: project,
androidBuildInfo: androidBuildInfo, androidBuildInfo: androidBuildInfo,
target: target, target: target,
isBuildingBundle: isBuildingBundle, isBuildingBundle: isBuildingBundle,
localGradleErrors: localGradleErrors, localGradleErrors: localGradleErrors,
retries: retries - 1, retry: retry,
maxRetries: maxRetries,
); );
final String successEventLabel = 'gradle-${detectedGradleError!.eventLabel}-success';
BuildEvent(successEventLabel, type: 'gradle', flutterUsage: _usage).send(); BuildEvent(successEventLabel, type: 'gradle', flutterUsage: _usage).send();
return; return;
case GradleBuildStatus.exit: case GradleBuildStatus.exit:
// noop. // Continue and throw tool exit.
} }
} }
BuildEvent('gradle-${detectedGradleError?.eventLabel}-failure', type: 'gradle', flutterUsage: _usage).send(); BuildEvent('gradle-${detectedGradleError?.eventLabel}-failure', type: 'gradle', flutterUsage: _usage).send();
@ -433,7 +446,6 @@ class AndroidGradleBuilder implements AndroidBuilder {
exitCode: exitCode, exitCode: exitCode,
); );
} }
}
if (isBuildingBundle) { if (isBuildingBundle) {
final File bundleFile = findBundleFile(project, buildInfo, _logger, _usage); final File bundleFile = findBundleFile(project, buildInfo, _logger, _usage);

View File

@ -226,8 +226,8 @@ final GradleHandledError networkErrorHandler = GradleHandledError(
required bool multidexEnabled, required bool multidexEnabled,
}) async { }) async {
globals.printError( globals.printError(
'${globals.logger.terminal.warningMark} Gradle threw an error while downloading artifacts from the network. ' '${globals.logger.terminal.warningMark} '
'Retrying to download...' 'Gradle threw an error while downloading artifacts from the network.'
); );
try { try {
final String? homeDir = globals.platform.environment['HOME']; final String? homeDir = globals.platform.environment['HOME'];

View File

@ -138,7 +138,8 @@ void main() {
gradleUtils: FakeGradleUtils(), gradleUtils: FakeGradleUtils(),
platform: FakePlatform(), platform: FakePlatform(),
); );
processManager.addCommand(const FakeCommand(
const FakeCommand fakeCmd = FakeCommand(
command: <String>[ command: <String>[
'gradlew', 'gradlew',
'-q', '-q',
@ -151,23 +152,15 @@ void main() {
'assembleRelease', 'assembleRelease',
], ],
exitCode: 1, exitCode: 1,
stderr: '\nSome gradle message\n' stderr: '\nSome gradle message\n',
)); );
processManager.addCommand(const FakeCommand(
command: <String>[ processManager.addCommand(fakeCmd);
'gradlew',
'-q', const int maxRetries = 2;
'-Ptarget-platform=android-arm,android-arm64,android-x64', for (int i = 0; i < maxRetries; i++) {
'-Ptarget=lib/main.dart', processManager.addCommand(fakeCmd);
'-Pbase-application-name=io.flutter.app.FlutterApplication', }
'-Pdart-obfuscation=false',
'-Ptrack-widget-creation=false',
'-Ptree-shake-icons=false',
'assembleRelease',
],
exitCode: 1,
stderr: '\nSome gradle message\n'
));
fileSystem.directory('android') fileSystem.directory('android')
.childFile('build.gradle') .childFile('build.gradle')
@ -186,6 +179,7 @@ void main() {
int testFnCalled = 0; int testFnCalled = 0;
await expectLater(() async { await expectLater(() async {
await builder.buildGradleApp( await builder.buildGradleApp(
maxRetries: maxRetries,
project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory),
androidBuildInfo: const AndroidBuildInfo( androidBuildInfo: const AndroidBuildInfo(
BuildInfo( BuildInfo(
@ -221,7 +215,10 @@ void main() {
message: 'Gradle task assembleRelease failed with exit code 1' message: 'Gradle task assembleRelease failed with exit code 1'
)); ));
expect(testFnCalled, equals(2)); expect(logger.statusText, contains('Retrying Gradle Build: #1, wait time: 100ms'));
expect(logger.statusText, contains('Retrying Gradle Build: #2, wait time: 200ms'));
expect(testFnCalled, equals(maxRetries + 1));
expect(testUsage.events, contains( expect(testUsage.events, contains(
const TestUsageEvent( const TestUsageEvent(
'build', 'build',

View File

@ -103,7 +103,6 @@ at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)''';
expect(testLogger.errorText, expect(testLogger.errorText,
contains( contains(
'Gradle threw an error while downloading artifacts from the network.' 'Gradle threw an error while downloading artifacts from the network.'
'Retrying to download...'
) )
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
@ -134,7 +133,6 @@ at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)''';
expect(testLogger.errorText, expect(testLogger.errorText,
contains( contains(
'Gradle threw an error while downloading artifacts from the network.' 'Gradle threw an error while downloading artifacts from the network.'
'Retrying to download...'
) )
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
@ -156,7 +154,6 @@ Exception in thread "main" java.lang.RuntimeException: Timeout of 120000 reached
expect(testLogger.errorText, expect(testLogger.errorText,
contains( contains(
'Gradle threw an error while downloading artifacts from the network.' 'Gradle threw an error while downloading artifacts from the network.'
'Retrying to download...'
) )
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
@ -194,7 +191,6 @@ Exception in thread "main" javax.net.ssl.SSLHandshakeException: Remote host clos
expect(testLogger.errorText, expect(testLogger.errorText,
contains( contains(
'Gradle threw an error while downloading artifacts from the network.' 'Gradle threw an error while downloading artifacts from the network.'
'Retrying to download...'
) )
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
@ -224,7 +220,6 @@ Exception in thread "main" java.io.FileNotFoundException: https://downloads.grad
expect(testLogger.errorText, expect(testLogger.errorText,
contains( contains(
'Gradle threw an error while downloading artifacts from the network.' 'Gradle threw an error while downloading artifacts from the network.'
'Retrying to download...'
) )
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
@ -265,7 +260,6 @@ Exception in thread "main" java.net.SocketException: Connection reset
expect(testLogger.errorText, expect(testLogger.errorText,
contains( contains(
'Gradle threw an error while downloading artifacts from the network.' 'Gradle threw an error while downloading artifacts from the network.'
'Retrying to download...'
) )
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
@ -293,7 +287,6 @@ A problem occurred configuring root project 'android'.
expect(testLogger.errorText, expect(testLogger.errorText,
contains( contains(
'Gradle threw an error while downloading artifacts from the network.' 'Gradle threw an error while downloading artifacts from the network.'
'Retrying to download...'
) )
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
@ -325,7 +318,6 @@ A problem occurred configuring root project 'android'.
expect(testLogger.errorText, expect(testLogger.errorText,
contains( contains(
'Gradle threw an error while downloading artifacts from the network.' 'Gradle threw an error while downloading artifacts from the network.'
'Retrying to download...'
) )
); );
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{