blob: 78673e69164f3423ae619b1e7f8f81947d3330d0 [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:appengine/appengine.dart';
import 'package:gcloud/db.dart';
import 'package:googleapis/bigquery/v2.dart';
import 'package:meta/meta.dart';
import '../datastore/cocoon_config.dart';
import '../model/appengine/commit.dart';
import '../model/appengine/key_helper.dart';
import '../model/appengine/task.dart';
import '../model/appengine/time_series.dart';
import '../model/appengine/time_series_value.dart';
import '../request_handling/api_request_handler.dart';
import '../request_handling/authentication.dart';
import '../request_handling/body.dart';
import '../request_handling/exceptions.dart';
import '../service/datastore.dart';
@immutable
class UpdateTaskStatus extends ApiRequestHandler<UpdateTaskStatusResponse> {
const UpdateTaskStatus(
Config config,
AuthenticationProvider authenticationProvider, {
@visibleForTesting
this.datastoreProvider = DatastoreService.defaultProvider,
}) : super(config: config, authenticationProvider: authenticationProvider);
final DatastoreServiceProvider datastoreProvider;
static const String taskKeyParam = 'TaskKey';
static const String newStatusParam = 'NewStatus';
static const String resultsParam = 'ResultData';
static const String scoreKeysParam = 'BenchmarkScoreKeys';
/// const variables for [BigQuery] operations
static const String projectId = 'flutter-dashboard';
static const String dataset = 'cocoon';
static const String table = 'Task';
@override
Future<UpdateTaskStatusResponse> post() async {
checkRequiredParameters(<String>[taskKeyParam, newStatusParam]);
final DatastoreService datastore = datastoreProvider(config.db);
final ClientContext clientContext = authContext.clientContext;
final KeyHelper keyHelper =
KeyHelper(applicationContext: clientContext.applicationContext);
final String newStatus = requestData[newStatusParam] as String;
final Map<String, dynamic> resultData =
requestData[resultsParam] as Map<String, dynamic> ??
const <String, dynamic>{};
final List<String> scoreKeys =
(requestData[scoreKeysParam] as List<dynamic>)?.cast<String>() ??
const <String>[];
Key taskKey;
try {
taskKey = keyHelper.decode(requestData[taskKeyParam] as String);
} catch (error) {
throw BadRequestException('Bad task key: ${requestData[taskKeyParam]}');
}
if (newStatus != Task.statusSucceeded && newStatus != Task.statusFailed) {
throw const BadRequestException(
'NewStatus can be one of "Succeeded", "Failed"');
}
final Task task = await datastore.db.lookupValue<Task>(taskKey, orElse: () {
throw BadRequestException('No such task: ${taskKey.id}');
});
final Commit commit =
await datastore.db.lookupValue<Commit>(task.commitKey, orElse: () {
throw BadRequestException('No such task: ${task.commitKey}');
});
if (newStatus == Task.statusFailed) {
// Attempt to de-flake the test.
final int maxRetries = config.maxTaskRetries;
if (task.attempts >= maxRetries) {
task.status = Task.statusFailed;
task.reason = 'Task failed on agent';
task.endTimestamp = DateTime.now().millisecondsSinceEpoch;
} else {
// This will cause this task to be picked up by an agent again.
task.status = Task.statusNew;
task.startTimestamp = 0;
}
} else {
task.status = newStatus;
task.endTimestamp = DateTime.now().millisecondsSinceEpoch;
}
await datastore.insert(<Task>[task]);
if (task.endTimestamp > 0) {
await _insertBigquery(commit, task);
}
// TODO(tvolkert): PushBuildStatusToGithub
for (String scoreKey in scoreKeys) {
final TimeSeries series =
await _getOrCreateTimeSeries(task, scoreKey, datastore);
final num value = resultData[scoreKey] as num;
final TimeSeriesValue seriesValue = TimeSeriesValue(
key: series.key.append(TimeSeriesValue),
createTimestamp: DateTime.now().millisecondsSinceEpoch,
revision: commit.sha,
branch: commit.branch,
taskKey: task.key,
value: value.toDouble(),
);
await datastore.insert(<TimeSeriesValue>[seriesValue]);
}
return UpdateTaskStatusResponse(task);
}
Future<void> _insertBigquery(Commit commit, Task task) async {
final TabledataResourceApi tabledataResourceApi =
await config.createTabledataResourceApi();
final List<Map<String, Object>> requestRows = <Map<String, Object>>[];
requestRows.add(<String, Object>{
'json': <String, Object>{
'ID': 'flutter/flutter/${commit.branch}/${commit.sha}',
'CreateTimestamp': task.createTimestamp,
'StartTimestamp': task.startTimestamp,
'EndTimestamp': task.endTimestamp,
'Name': task.name,
'Attempts': task.attempts,
'IsFlaky': task.isFlaky,
'TimeoutInMinutes': task.timeoutInMinutes,
'RequiredCapabilities': task.requiredCapabilities,
'ReservedForAgentID': task.reservedForAgentId,
'StageName': task.stageName,
'Status': task.status,
},
});
/// [rows] to be inserted to [BigQuery]
final TableDataInsertAllRequest request =
TableDataInsertAllRequest.fromJson(
<String, Object>{'rows': requestRows});
try {
await tabledataResourceApi.insertAll(request, projectId, dataset, table);
} on ApiRequestError {
log.warning('Failed to add ${task.name} to BigQuery: $ApiRequestError');
}
}
Future<TimeSeries> _getOrCreateTimeSeries(
Task task,
String scoreKey,
DatastoreService datastore,
) async {
final String id = '${task.name}.$scoreKey';
final Key timeSeriesKey =
Key.emptyKey(Partition(null)).append(TimeSeries, id: id);
TimeSeries series =
(await datastore.lookupByKey<TimeSeries>(<Key>[timeSeriesKey])).single;
if (series == null) {
series = TimeSeries(
key: timeSeriesKey,
timeSeriesId: id,
taskName: task.name,
label: scoreKey,
unit: 'ms',
);
await datastore.insert(<TimeSeries>[series]);
}
return series;
}
}
@immutable
class UpdateTaskStatusResponse extends JsonBody {
const UpdateTaskStatusResponse(this.task) : assert(task != null);
final Task task;
@override
Map<String, dynamic> toJson() {
return <String, dynamic>{
'Name': task.name,
'Status': task.status,
};
}
}