blob: 2b093854c46cf917e83cb6da408290fef55ecbe4 [file] [log] [blame]
// Copyright 2019 The Flutter 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:github/github.dart';
import 'package:meta/meta.dart';
import '../../ci_yaml.dart';
import '../foundation/providers.dart';
import '../foundation/typedefs.dart';
import '../foundation/utils.dart';
import '../model/appengine/commit.dart';
import '../model/appengine/task.dart';
import '../request_handling/api_request_handler.dart';
import '../request_handling/authentication.dart';
import '../request_handling/body.dart';
import '../service/buildbucket.dart';
import '../service/config.dart';
import '../service/datastore.dart';
import '../service/logging.dart';
import '../service/luci.dart';
import '../service/luci_build_service.dart';
import '../service/scheduler.dart';
@immutable
class RefreshChromebotStatus extends ApiRequestHandler<Body> {
const RefreshChromebotStatus(
Config config,
AuthenticationProvider authenticationProvider,
this.luciBuildService, {
required this.scheduler,
@visibleForTesting LuciServiceProvider? luciServiceProvider,
@visibleForTesting DatastoreServiceProvider? datastoreProvider,
@visibleForTesting this.branchHttpClientProvider = Providers.freshHttpClient,
@visibleForTesting this.gitHubBackoffCalculator = twoSecondLinearBackoff,
}) : luciServiceProvider = luciServiceProvider ?? _createLuciService,
datastoreProvider = datastoreProvider ?? DatastoreService.defaultProvider,
super(config: config, authenticationProvider: authenticationProvider);
final LuciBuildService luciBuildService;
final LuciServiceProvider luciServiceProvider;
final DatastoreServiceProvider datastoreProvider;
final HttpClientProvider branchHttpClientProvider;
final GitHubBackoffCalculator gitHubBackoffCalculator;
final Scheduler scheduler;
static const String kRepoParam = 'repo';
static LuciService _createLuciService(ApiRequestHandler<dynamic> handler) {
return LuciService(
buildBucketClient: BuildBucketClient(),
config: handler.config,
clientContext: handler.authContext!.clientContext,
);
}
@override
Future<Body> get() async {
final String repoName = request!.uri.queryParameters[kRepoParam] ?? Config.flutterSlug.name;
final RepositorySlug slug = RepositorySlug('flutter', repoName);
final LuciService luciService = luciServiceProvider(this);
final DatastoreService datastore = datastoreProvider(config.db);
final Commit latestCommit = await datastore.queryRecentCommits(limit: 1, slug: slug).single;
final CiYaml ciYaml = await scheduler.getCiYaml(latestCommit);
final List<LuciBuilder> postsubmitBuilders = await scheduler.getPostSubmitBuilders(ciYaml);
final Map<BranchLuciBuilder, Map<String, List<LuciTask>>> luciTasks = await luciService.getBranchRecentTasks(
builders: postsubmitBuilders,
requireTaskName: true,
);
log.fine('${luciTasks.keys.length} builders retrieved');
for (BranchLuciBuilder branchLuciBuilder in luciTasks.keys) {
await runTransactionWithRetries(() async {
await _updateStatus(
branchLuciBuilder.luciBuilder!,
branchLuciBuilder.branch,
datastore,
luciTasks[branchLuciBuilder],
slug,
);
});
}
return Body.empty;
}
/// Update chromebot tasks statuses in datastore for [builder],
/// based on latest [luciTasks] statuses.
Future<void> _updateStatus(
LuciBuilder builder,
String? branch,
DatastoreService datastore,
Map<String, List<LuciTask>>? luciTasksMap,
RepositorySlug slug,
) async {
final List<FullTask> datastoreTasks = await datastore
.queryRecentTasks(
taskName: builder.taskName,
branch: branch,
slug: slug,
)
.toList();
/// Update [devicelabTask] when first [luciTask] run finishes. There may be
/// reruns for the same commit and same builder. Update [devicelabTask]
/// [builderNumberList] when luci rerun happens, and update [devicelabTask]
/// status when the status of latest luci run changes.
for (FullTask datastoreTask in datastoreTasks) {
final String commitSha = datastoreTask.commit.sha!;
if (!luciTasksMap!.containsKey(commitSha)) {
continue;
}
final List<LuciTask> luciTasks = luciTasksMap[commitSha]!;
final String buildNumberList =
luciTasks.reversed.map((LuciTask luciTask) => luciTask.buildNumber.toString()).toList().join(',');
final LuciTask latestLuciTask = luciTasks.first;
if (buildNumberList != datastoreTask.task.buildNumberList || latestLuciTask.status != datastoreTask.task.status) {
final Task update = datastoreTask.task;
update.status = latestLuciTask.status;
final CiYaml ciYaml = await scheduler.getCiYaml(datastoreTask.commit);
final Target target =
ciYaml.postsubmitTargets.singleWhere((Target target) => target.value.name == datastoreTask.task.name);
await luciBuildService.checkRerunBuilder(
commit: datastoreTask.commit,
target: target,
task: update,
datastore: datastore,
);
update.luciBucket = builder.flaky ?? false ? 'luci.flutter.staging' : 'luci.flutter.prod';
update.buildNumberList = buildNumberList;
await datastore.insert(<Task>[update]);
// Save luci task record to BigQuery only when task finishes.
if (update.status == Task.statusFailed || update.status == Task.statusSucceeded) {
await _insertBigquery(update);
}
}
}
}
Future<void> _insertBigquery(Task task) async {
const String bigqueryTableName = 'Task';
final Map<String, dynamic> bigqueryData = <String, dynamic>{
'ID': task.commitKey?.id,
'CreateTimestamp': task.createTimestamp,
'StartTimestamp': task.startTimestamp,
'EndTimestamp': task.endTimestamp,
'Name': task.name,
'Attempts': task.attempts,
'IsFlaky': task.isFlaky,
'TimeoutInMinutes': task.timeoutInMinutes ?? 0,
'RequiredCapabilities': task.requiredCapabilities ?? <String>[],
'ReservedForAgentID': task.reservedForAgentId,
'StageName': task.stageName ?? 'unknown',
'Status': task.status,
};
await insertBigquery(bigqueryTableName, bigqueryData, await config.createTabledataResourceApi());
}
}