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,38 +407,44 @@ 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(
line: detectedGradleErrorLine!,
project: project,
usesAndroidX: usesAndroidX,
multidexEnabled: androidBuildInfo.multidexEnabled,
);
if (retries >= 1) {
final String successEventLabel = 'gradle-${detectedGradleError!.eventLabel}-success';
switch (status) {
case GradleBuildStatus.retry:
await buildGradleApp(
project: project,
androidBuildInfo: androidBuildInfo,
target: target,
isBuildingBundle: isBuildingBundle,
localGradleErrors: localGradleErrors,
retries: retries - 1,
);
BuildEvent(successEventLabel, type: 'gradle', flutterUsage: _usage).send();
return;
case GradleBuildStatus.exit:
// noop.
}
}
BuildEvent('gradle-${detectedGradleError?.eventLabel}-failure', type: 'gradle', flutterUsage: _usage).send();
throwToolExit(
'Gradle task $assembleTask failed with exit code $exitCode',
exitCode: exitCode,
);
} }
final GradleBuildStatus status = await detectedGradleError!.handler(
line: detectedGradleErrorLine!,
project: project,
usesAndroidX: usesAndroidX,
multidexEnabled: androidBuildInfo.multidexEnabled,
);
if (maxRetries == null || retry < maxRetries) {
switch (status) {
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(
project: project,
androidBuildInfo: androidBuildInfo,
target: target,
isBuildingBundle: isBuildingBundle,
localGradleErrors: localGradleErrors,
retry: retry,
maxRetries: maxRetries,
);
final String successEventLabel = 'gradle-${detectedGradleError!.eventLabel}-success';
BuildEvent(successEventLabel, type: 'gradle', flutterUsage: _usage).send();
return;
case GradleBuildStatus.exit:
// Continue and throw tool exit.
}
}
BuildEvent('gradle-${detectedGradleError?.eventLabel}-failure', type: 'gradle', flutterUsage: _usage).send();
throwToolExit(
'Gradle task $assembleTask failed with exit code $exitCode',
exitCode: exitCode,
);
} }
if (isBuildingBundle) { if (isBuildingBundle) {

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

@ -102,8 +102,7 @@ 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>{
@ -133,8 +132,7 @@ 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>{
@ -155,8 +153,7 @@ 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>{
@ -193,8 +190,7 @@ 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>{
@ -223,8 +219,7 @@ 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>{
@ -264,8 +259,7 @@ 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>{
@ -292,8 +286,7 @@ 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>{
@ -324,8 +317,7 @@ 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>{