Unverified Commit c8266d34 authored by Emmanuel Garcia's avatar Emmanuel Garcia Committed by GitHub

Improve Gradle retry logic (#96554)

parent f9921ebc
...@@ -2,6 +2,8 @@ ...@@ -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) { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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( final GradleBuildStatus status = await detectedGradleError!.handler(
line: detectedGradleErrorLine!, line: detectedGradleErrorLine!,
project: project, project: project,
usesAndroidX: usesAndroidX, usesAndroidX: usesAndroidX,
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.
await buildGradleApp( // The expected wait times are: 100ms, 200ms, 400ms, and so on...
project: project, final int waitTime = min(pow(2, retry).toInt() * 100, kMaxRetryTime.inMicroseconds);
androidBuildInfo: androidBuildInfo, retry += 1;
target: target, _logger.printStatus('Retrying Gradle Build: #$retry, wait time: ${waitTime}ms');
isBuildingBundle: isBuildingBundle, await Future<void>.delayed(Duration(milliseconds: waitTime));
localGradleErrors: localGradleErrors, await buildGradleApp(
retries: retries - 1, project: project,
); androidBuildInfo: androidBuildInfo,
BuildEvent(successEventLabel, type: 'gradle', flutterUsage: _usage).send(); target: target,
return; isBuildingBundle: isBuildingBundle,
case GradleBuildStatus.exit: localGradleErrors: localGradleErrors,
// noop. 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,
);
} }
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) {
......
...@@ -226,8 +226,8 @@ final GradleHandledError networkErrorHandler = GradleHandledError( ...@@ -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'];
......
...@@ -138,22 +138,8 @@ void main() { ...@@ -138,22 +138,8 @@ void main() {
gradleUtils: FakeGradleUtils(), gradleUtils: FakeGradleUtils(),
platform: FakePlatform(), platform: FakePlatform(),
); );
processManager.addCommand(const FakeCommand(
command: <String>[ const FakeCommand fakeCmd = FakeCommand(
'gradlew',
'-q',
'-Ptarget-platform=android-arm,android-arm64,android-x64',
'-Ptarget=lib/main.dart',
'-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'
));
processManager.addCommand(const FakeCommand(
command: <String>[ command: <String>[
'gradlew', 'gradlew',
'-q', '-q',
...@@ -166,8 +152,15 @@ void main() { ...@@ -166,8 +152,15 @@ void main() {
'assembleRelease', 'assembleRelease',
], ],
exitCode: 1, exitCode: 1,
stderr: '\nSome gradle message\n' stderr: '\nSome gradle message\n',
)); );
processManager.addCommand(fakeCmd);
const int maxRetries = 2;
for (int i = 0; i < maxRetries; i++) {
processManager.addCommand(fakeCmd);
}
fileSystem.directory('android') fileSystem.directory('android')
.childFile('build.gradle') .childFile('build.gradle')
...@@ -186,6 +179,7 @@ void main() { ...@@ -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() { ...@@ -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',
......
...@@ -102,8 +102,7 @@ at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)'''; ...@@ -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)'''; ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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'. ...@@ -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'. ...@@ -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>{
......
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