blob: 1a70e807f1fdc925d309943e93460a4b44c47253 [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 'package:cocoon_service/ci_yaml.dart';
import 'package:gcloud/db.dart';
import 'package:meta/meta.dart';
import '../model/appengine/commit.dart';
import '../model/appengine/task.dart';
import '../model/luci/push_message.dart';
import '../request_handling/body.dart';
import '../request_handling/exceptions.dart';
import '../request_handling/subscription_handler.dart';
import '../service/datastore.dart';
import '../service/logging.dart';
import '../service/github_checks_service.dart';
import '../service/scheduler.dart';
/// An endpoint for listening to build updates for postsubmit builds.
///
/// The PubSub subscription is set up here:
/// https://cloud.google.com/cloudpubsub/subscription/detail/luci-postsubmit?project=flutter-dashboard&tab=overview
///
/// This endpoint is responsible for updating Datastore with the result of builds from LUCI.
@immutable
class PostsubmitLuciSubscription extends SubscriptionHandler {
/// Creates an endpoint for listening to LUCI status updates.
const PostsubmitLuciSubscription({
required super.cache,
required super.config,
super.authProvider,
@visibleForTesting this.datastoreProvider = DatastoreService.defaultProvider,
required this.scheduler,
required this.githubChecksService,
}) : super(subscriptionName: 'luci-postsubmit');
final DatastoreServiceProvider datastoreProvider;
final Scheduler scheduler;
final GithubChecksService githubChecksService;
@override
Future<Body> post() async {
final DatastoreService datastore = datastoreProvider(config.db);
final BuildPushMessage buildPushMessage = BuildPushMessage.fromPushMessage(message);
log.fine('userData=${buildPushMessage.userData}');
log.fine('Updating buildId=${buildPushMessage.build?.id} for result=${buildPushMessage.build?.result}');
if (buildPushMessage.userData.isEmpty) {
log.fine('User data is empty');
return Body.empty;
}
final String? rawTaskKey = buildPushMessage.userData['task_key'] as String?;
final String? rawCommitKey = buildPushMessage.userData['commit_key'] as String?;
if (rawCommitKey == null) {
throw const BadRequestException('userData does not contain commit_key');
}
final Build? build = buildPushMessage.build;
if (build == null) {
log.warning('Build is null');
return Body.empty;
}
final Key<String> commitKey = Key<String>(Key<dynamic>.emptyKey(Partition(null)), Commit, rawCommitKey);
Task? task;
if (rawTaskKey == null || rawTaskKey.isEmpty || rawTaskKey == 'null') {
log.fine('Pulling builder name from parameters_json...');
log.fine(build.buildParameters);
final String? taskName = build.buildParameters?['builder_name'] as String?;
if (taskName == null || taskName.isEmpty) {
throw const BadRequestException('task_key is null and parameters_json does not contain the builder name');
}
final List<Task> tasks = await datastore.queryRecentTasksByName(name: taskName).toList();
task = tasks.singleWhere((Task task) => task.parentKey?.id == commitKey.id);
} else {
log.fine('Looking up key...');
final int taskId = int.parse(rawTaskKey);
final Key<int> taskKey = Key<int>(commitKey, Task, taskId);
task = await datastore.lookupByValue<Task>(taskKey);
}
log.fine('Found $task');
if (_shouldUpdateTask(build, task)) {
final String oldTaskStatus = task.status;
task.updateFromBuild(build);
await datastore.insert(<Task>[task]);
log.fine('Updated datastore from $oldTaskStatus to ${task.status}');
} else {
log.fine('skip processing for build with status scheduled or task with status finished.');
}
final Commit commit = await datastore.lookupByValue<Commit>(commitKey);
final CiYaml ciYaml = await scheduler.getCiYaml(commit);
final List<Target> postsubmitTargets = ciYaml.postsubmitTargets;
if (!postsubmitTargets.any((element) => element.value.name == task!.name)) {
log.warning('Target ${task.name} has been deleted from TOT. Skip updating.');
return Body.empty;
}
final Target target = postsubmitTargets.singleWhere((Target target) => target.value.name == task!.name);
if (task.status == Task.statusFailed ||
task.status == Task.statusInfraFailure ||
task.status == Task.statusCancelled) {
log.fine('Trying to auto-retry...');
final bool retried = await scheduler.luciBuildService.checkRerunBuilder(
commit: commit,
target: target,
task: task,
datastore: datastore,
);
log.info('Retried: $retried');
}
// Only update GitHub checks if target is not bringup
if (target.value.bringup == false && config.postsubmitSupportedRepos.contains(target.slug)) {
log.info('Updating check status for ${target.getTestName}');
await githubChecksService.updateCheckStatus(
buildPushMessage,
scheduler.luciBuildService,
commit.slug,
);
}
return Body.empty;
}
// No need to update task in datastore if
// 1) the build is `scheduled`. Task is marked as `In Progress`
// whenever scheduled, either from scheduler/backfiller/rerun. We need to update
// task in datastore only for
// a) `started`: update info like builder number.
// b) `completed`: update info like status.
// 2) the task is already completed.
// The task may have been marked as completed from test framework via update-task-status API.
bool _shouldUpdateTask(Build build, Task task) {
return build.status != Status.scheduled && !Task.finishedStatusValues.contains(task.status);
}
}