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 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math';
import 'package:crypto/crypto.dart';
import 'package:meta/meta.dart';
import 'package:process/process.dart';
......@@ -106,6 +108,9 @@ Iterable<String> _apkFilesFor(AndroidBuildInfo androidBuildInfo) {
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.
class AndroidGradleBuilder implements AndroidBuilder {
AndroidGradleBuilder({
......@@ -212,7 +217,7 @@ class AndroidGradleBuilder implements AndroidBuilder {
/// * [target] is the target dart entry point. Typically, `lib/main.dart`.
/// * If [isBuildingBundle] is `true`, then the output artifact is an `*.aab`,
/// 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({
required FlutterProject project,
required AndroidBuildInfo androidBuildInfo,
......@@ -221,7 +226,8 @@ class AndroidGradleBuilder implements AndroidBuilder {
required List<GradleHandledError> localGradleErrors,
bool validateDeferredComponents = true,
bool deferredComponentsEnabled = false,
int retries = 1,
int retry = 0,
@visibleForTesting int? maxRetries,
}) async {
assert(project != null);
assert(androidBuildInfo != null);
......@@ -401,7 +407,7 @@ class AndroidGradleBuilder implements AndroidBuilder {
'Gradle task $assembleTask failed with exit code $exitCode',
exitCode: exitCode,
);
} else {
}
final GradleBuildStatus status = await detectedGradleError!.handler(
line: detectedGradleErrorLine!,
project: project,
......@@ -409,22 +415,29 @@ class AndroidGradleBuilder implements AndroidBuilder {
multidexEnabled: androidBuildInfo.multidexEnabled,
);
if (retries >= 1) {
final String successEventLabel = 'gradle-${detectedGradleError!.eventLabel}-success';
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,
retries: retries - 1,
retry: retry,
maxRetries: maxRetries,
);
final String successEventLabel = 'gradle-${detectedGradleError!.eventLabel}-success';
BuildEvent(successEventLabel, type: 'gradle', flutterUsage: _usage).send();
return;
case GradleBuildStatus.exit:
// noop.
// Continue and throw tool exit.
}
}
BuildEvent('gradle-${detectedGradleError?.eventLabel}-failure', type: 'gradle', flutterUsage: _usage).send();
......@@ -433,7 +446,6 @@ class AndroidGradleBuilder implements AndroidBuilder {
exitCode: exitCode,
);
}
}
if (isBuildingBundle) {
final File bundleFile = findBundleFile(project, buildInfo, _logger, _usage);
......
......@@ -226,8 +226,8 @@ final GradleHandledError networkErrorHandler = GradleHandledError(
required bool multidexEnabled,
}) async {
globals.printError(
'${globals.logger.terminal.warningMark} Gradle threw an error while downloading artifacts from the network. '
'Retrying to download...'
'${globals.logger.terminal.warningMark} '
'Gradle threw an error while downloading artifacts from the network.'
);
try {
final String? homeDir = globals.platform.environment['HOME'];
......
......@@ -138,22 +138,8 @@ void main() {
gradleUtils: FakeGradleUtils(),
platform: FakePlatform(),
);
processManager.addCommand(const FakeCommand(
command: <String>[
'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(
const FakeCommand fakeCmd = FakeCommand(
command: <String>[
'gradlew',
'-q',
......@@ -166,8 +152,15 @@ void main() {
'assembleRelease',
],
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')
.childFile('build.gradle')
......@@ -186,6 +179,7 @@ void main() {
int testFnCalled = 0;
await expectLater(() async {
await builder.buildGradleApp(
maxRetries: maxRetries,
project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory),
androidBuildInfo: const AndroidBuildInfo(
BuildInfo(
......@@ -221,7 +215,10 @@ void main() {
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(
const TestUsageEvent(
'build',
......
......@@ -102,8 +102,7 @@ at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)''';
expect(testLogger.errorText,
contains(
'Gradle threw an error while downloading artifacts from the network. '
'Retrying to download...'
'Gradle threw an error while downloading artifacts from the network.'
)
);
}, overrides: <Type, Generator>{
......@@ -133,8 +132,7 @@ at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)''';
expect(testLogger.errorText,
contains(
'Gradle threw an error while downloading artifacts from the network. '
'Retrying to download...'
'Gradle threw an error while downloading artifacts from the network.'
)
);
}, overrides: <Type, Generator>{
......@@ -155,8 +153,7 @@ Exception in thread "main" java.lang.RuntimeException: Timeout of 120000 reached
expect(testLogger.errorText,
contains(
'Gradle threw an error while downloading artifacts from the network. '
'Retrying to download...'
'Gradle threw an error while downloading artifacts from the network.'
)
);
}, overrides: <Type, Generator>{
......@@ -193,8 +190,7 @@ Exception in thread "main" javax.net.ssl.SSLHandshakeException: Remote host clos
expect(testLogger.errorText,
contains(
'Gradle threw an error while downloading artifacts from the network. '
'Retrying to download...'
'Gradle threw an error while downloading artifacts from the network.'
)
);
}, overrides: <Type, Generator>{
......@@ -223,8 +219,7 @@ Exception in thread "main" java.io.FileNotFoundException: https://downloads.grad
expect(testLogger.errorText,
contains(
'Gradle threw an error while downloading artifacts from the network. '
'Retrying to download...'
'Gradle threw an error while downloading artifacts from the network.'
)
);
}, overrides: <Type, Generator>{
......@@ -264,8 +259,7 @@ Exception in thread "main" java.net.SocketException: Connection reset
expect(testLogger.errorText,
contains(
'Gradle threw an error while downloading artifacts from the network. '
'Retrying to download...'
'Gradle threw an error while downloading artifacts from the network.'
)
);
}, overrides: <Type, Generator>{
......@@ -292,8 +286,7 @@ A problem occurred configuring root project 'android'.
expect(testLogger.errorText,
contains(
'Gradle threw an error while downloading artifacts from the network. '
'Retrying to download...'
'Gradle threw an error while downloading artifacts from the network.'
)
);
}, overrides: <Type, Generator>{
......@@ -324,8 +317,7 @@ A problem occurred configuring root project 'android'.
expect(testLogger.errorText,
contains(
'Gradle threw an error while downloading artifacts from the network. '
'Retrying to download...'
'Gradle threw an error while downloading artifacts from the network.'
)
);
}, 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