blob: 7c5630f30ff5799d04fea2f5dfd98af09007ef68 [file] [log] [blame]
// Copyright 2019 The Chromium 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:gcloud/db.dart';
import 'package:github/server.dart';
import 'package:meta/meta.dart';
import '../model/appengine/commit.dart';
import '../model/appengine/github_build_status_update.dart';
import '../model/appengine/github_gold_status_update.dart';
import '../model/appengine/stage.dart';
import '../model/appengine/task.dart';
import '../model/appengine/time_series.dart';
import '../model/appengine/time_series_value.dart';
typedef DatastoreServiceProvider = DatastoreService Function();
/// Service class for interacting with App Engine cloud datastore.
///
/// This service exists to provide an API for common datastore queries made by
/// the Cocoon backend.
@immutable
class DatastoreService {
/// Creates a new [DatastoreService].
///
/// The [db] argument must not be null.
const DatastoreService({
@required this.db,
}) : assert(db != null);
/// The backing [DatastoreDB] object. Guaranteed to be non-null.
final DatastoreDB db;
static DatastoreService defaultProvider() {
return DatastoreService(db: dbService);
}
/// Queries for recent commits.
///
/// The [limit] argument specifies the maximum number of commits to retrieve.
///
/// The returned commits will be ordered by most recent [Commit.timestamp].
Stream<Commit> queryRecentCommits(
{int limit = 100, int timestamp, String branch}) {
timestamp ??= DateTime.now().millisecondsSinceEpoch;
branch ??= 'master';
final Query<Commit> query = db.query<Commit>()
..limit(limit)
..filter('branch =', branch)
..order('-timestamp')
..filter('timestamp <', timestamp);
return query.run();
}
// Queries for recent commits without considering branches.
// TODO(keyonghan): combine this function with the above `queryRecentCommits`,
// this needs to fix https://github.com/flutter/flutter/issues/52694.
Stream<Commit> queryRecentCommitsNoBranch({int limit = 100, int timestamp}) {
timestamp ??= DateTime.now().millisecondsSinceEpoch;
final Query<Commit> query = db.query<Commit>()
..limit(limit)
..order('-timestamp')
..filter('timestamp <', timestamp);
return query.run();
}
/// queryRecentTimeSerialsValues fetches the latest benchmark results starting from
/// [startFrom] and up to a given [limit].
///
/// If startFrom is nil, starts from the latest available record.
/// [startFrom] to be implemented...
Stream<TimeSeriesValue> queryRecentTimeSeriesValues(TimeSeries timeSeries,
{int limit = 1500, String startFrom}) {
final Query<TimeSeriesValue> query =
db.query<TimeSeriesValue>(ancestorKey: timeSeries.key)
..limit(limit)
..order('-createTimestamp');
return query.run();
}
/// Queries for recent tasks that meet the specified criteria.
///
/// Since each task belongs to a commit, this query implicitly includes a
/// query of the most recent N commits (see [queryRecentCommits]). The
/// [commitLimit] argument specifies how many commits to consider when
/// retrieving the list of recent tasks.
///
/// The [taskLimit] argument specifies how many tasks to retrieve for each
/// commit that is considered.
///
/// If [taskName] is specified, only tasks whose [Task.name] matches the
/// specified value will be returned. By default, tasks will be returned
/// regardless of their name.
///
/// The returned tasks will be ordered by most recent [Commit.timestamp]
/// first, then by most recent [Task.createTimestamp].
Stream<FullTask> queryRecentTasks({
String taskName,
int commitLimit = 20,
int taskLimit = 20,
}) async* {
assert(commitLimit != null);
assert(taskLimit != null);
await for (Commit commit in queryRecentCommits(limit: commitLimit)) {
final Query<Task> query = db.query<Task>(ancestorKey: commit.key)
..limit(taskLimit)
..order('-createTimestamp');
if (taskName != null) {
query.filter('name =', taskName);
}
yield* query.run().map<FullTask>((Task task) => FullTask(task, commit));
}
}
/// Finds all tasks owned by the specified [commit] and partitions them into
/// stages.
///
/// The returned list of stages will be ordered by the natural ordering of
/// [Stage].
Future<List<Stage>> queryTasksGroupedByStage(Commit commit) async {
final Query<Task> query = db.query<Task>(ancestorKey: commit.key)
..order('-stageName');
final Map<String, StageBuilder> stages = <String, StageBuilder>{};
await for (Task task in query.run()) {
if (!stages.containsKey(task.stageName)) {
stages[task.stageName] = StageBuilder()
..commit = commit
..name = task.stageName;
}
stages[task.stageName].tasks.add(task);
}
final List<Stage> result = stages.values
.map<Stage>((StageBuilder stage) => stage.build())
.toList();
return result..sort();
}
Future<GithubBuildStatusUpdate> queryLastStatusUpdate(
RepositorySlug slug,
PullRequest pr,
) async {
final Query<GithubBuildStatusUpdate> query = db
.query<GithubBuildStatusUpdate>()
..filter('repository =', slug.fullName)
..filter('pr =', pr.number)
..filter('head =', pr.head.sha);
final List<GithubBuildStatusUpdate> previousStatusUpdates =
await query.run().toList();
if (previousStatusUpdates.isEmpty) {
return GithubBuildStatusUpdate(
repository: slug.fullName,
pr: pr.number,
head: pr.head.sha,
status: null,
updates: 0,
);
} else {
if (previousStatusUpdates.length > 1) {
throw StateError(
'GithubBuildStatusUpdate should have no more than one entries on '
'repository ${slug.fullName}, pr ${pr.number}, head ${pr.head.sha}');
}
return previousStatusUpdates.single;
}
}
Future<GithubGoldStatusUpdate> queryLastGoldUpdate(
RepositorySlug slug,
PullRequest pr,
) async {
final Query<GithubGoldStatusUpdate> query = db
.query<GithubGoldStatusUpdate>()
..filter('repository =', slug.fullName)
..filter('pr =', pr.number);
final List<GithubGoldStatusUpdate> previousStatusUpdates =
await query.run().toList();
if (previousStatusUpdates.isEmpty) {
return GithubGoldStatusUpdate(
pr: pr.number,
head: '',
status: '',
updates: 0,
description: '',
repository: slug.fullName,
);
} else {
if (previousStatusUpdates.length > 1) {
throw StateError(
'GithubGoldStatusUpdate should have no more than one entry on '
'repository ${slug.fullName}, pr ${pr.number}.');
}
return previousStatusUpdates.single;
}
}
}