| // 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, |
| }; |
| } |
| } |