blob: bd8f64551366997bd1970e55a4d86835bf688b34 [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:meta/meta.dart';
import '../datastore/cocoon_config.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/datastore.dart';
/// Cleans up any tasks that have been [Task.statusInProgress] for over an hour.
///
/// It is assumed that any tasks that are still recorded as being in progress
/// an hour after having been started are in fact stranded, perhaps because the
/// agent running the task crashed. In response, this handler looks for such
/// tasks and either:
///
/// * Marks them as [Task.statusFailed] if they've reached their retry limit
/// * Marks them as [Task.statusNew] if they've not reached their retry limit,
/// to allow an agent to pick the task up and try to run it again.
@immutable
class VacuumClean extends ApiRequestHandler<Body> {
const VacuumClean(
Config config,
AuthenticationProvider authenticationProvider, {
@visibleForTesting DatastoreServiceProvider datastoreProvider,
}) : datastoreProvider =
datastoreProvider ?? DatastoreService.defaultProvider,
super(config: config, authenticationProvider: authenticationProvider);
final DatastoreServiceProvider datastoreProvider;
@override
Future<Body> get() async {
final int maxRetries = config.maxTaskRetries;
final DatastoreService datastore = datastoreProvider(config.db);
final List<Task> tasks = await datastore
.queryRecentTasksNoBranch(commitLimit: config.commitNumber)
.map<Task>((FullTask fullTask) => fullTask.task)
.where(shouldBeVacuumCleaned)
.toList();
log.debug(
'Found ${tasks.length} in progress tasks that have been stranded');
for (Task task in tasks) {
if (task.attempts >= maxRetries) {
task.status = Task.statusFailed;
task.reason = 'Task timed out after 1 hour';
} else {
// This will cause this task to be picked up by an agent again.
task.status = Task.statusNew;
task.startTimestamp = 0;
}
}
await datastore.insert(tasks);
return Body.empty;
}
bool shouldBeVacuumCleaned(Task task) {
return _inProgress(task) && isOverAnHourOld(task);
}
/// Returns whether [task] is in progress.
bool _inProgress(Task task) => task.status == Task.statusInProgress;
/// Returns whether the specified [task] was started over an hour ago.
bool isOverAnHourOld(Task task) {
final int now = DateTime.now().millisecondsSinceEpoch;
const Duration oneHour = Duration(hours: 1);
return task.startTimestamp < now - oneHour.inMilliseconds;
}
}