# Copyright 2017 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.
"""Monitor jobs and abort them as necessary.
This daemon does a number of upkeep tasks:
* When a process owning a job crashes, job_aborter will mark the job as
aborted in the database and clean up its lease files.
* When a job is marked aborted in the database, job_aborter will signal
the process owning the job to abort.
See also http://goto.google.com/monitor_db_per_job_refactor
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import argparse
import logging
import sys
import time
from lucifer import autotest
from lucifer import handoffs
from lucifer import leasing
from lucifer import loglib
logger = logging.getLogger(__name__)
def main(args):
"""Main function
@param args: list of command line args
"""
parser = argparse.ArgumentParser(prog='job_aborter', description=__doc__)
parser.add_argument('--jobdir', required=True)
loglib.add_logging_options(parser)
args = parser.parse_args(args)
loglib.configure_logging_with_args(parser, args)
logger.info('Starting with args: %r', args)
autotest.monkeypatch()
ts_mon_config = autotest.chromite_load('ts_mon_config')
with ts_mon_config.SetupTsMonGlobalState('job_aborter'):
_main_loop(jobdir=args.jobdir)
assert False # cannot exit normally
def _main_loop(jobdir):
transaction = autotest.deps_load('django.db.transaction')
@transaction.commit_manually
def flush_transaction():
"""Flush transaction https://stackoverflow.com/questions/3346124/"""
transaction.commit()
metrics = _Metrics()
metrics.send_starting()
while True:
logger.debug('Tick')
metrics.send_tick()
_main_loop_body(metrics, jobdir)
flush_transaction()
time.sleep(20)
def _main_loop_body(metrics, jobdir):
active_leases = {
lease.id: lease for lease in leasing.leases_iter(jobdir)
if not lease.expired()
}
_mark_expired_jobs_failed(metrics, active_leases)
_abort_timed_out_jobs(active_leases)
_abort_jobs_marked_aborting(active_leases)
_abort_special_tasks_marked_aborted()
_clean_up_expired_leases(jobdir)
# TODO(crbug.com/748234): abort_jobs_past_max_runtime goes into
# lucifer_run_job
def _mark_expired_jobs_failed(metrics, active_leases):
"""Mark expired jobs failed.
Expired jobs are jobs that have an incomplete JobHandoff and that do
not have an active lease. These jobs have been handed off to a
job_reporter, but that job_reporter has crashed. These jobs are
marked failed in the database.
@param metrics: _Metrics instance.
@param active_leases: dict mapping job ids to Leases.
"""
logger.debug('Looking for expired jobs')
job_ids = []
for handoff in handoffs.incomplete():
logger.debug('Found handoff: %d', handoff.job_id)
if handoff.job_id not in active_leases:
logger.debug('Handoff %d is missing active lease', handoff.job_id)
job_ids.append(handoff.job_id)
handoffs.clean_up(job_ids)
handoffs.mark_complete(job_ids)
metrics.send_expired_jobs(len(job_ids))
def _abort_timed_out_jobs(active_leases):
"""Send abort to timed out jobs.
@param active_leases: dict mapping job ids to Leases.
"""
for job in _timed_out_jobs_queryset():
if job.id in active_leases:
active_leases[job.id].maybe_abort()
def _abort_jobs_marked_aborting(active_leases):
"""Send abort to jobs marked aborting in Autotest database.
@param active_leases: dict mapping job ids to Leases.
"""
for job in _aborting_jobs_queryset():
if job.id in active_leases:
active_leases[job.id].maybe_abort()
def _abort_special_tasks_marked_aborted():
# TODO(crbug.com/748234): Special tasks not implemented yet. This
# would abort jobs running on the behalf of special tasks and thus
# need to check a different database table.
pass
def _clean_up_expired_leases(jobdir):
"""Clean up files for expired leases.
We only care about active leases, so we can remove the stale files
for expired leases.
"""
for lease in leasing.leases_iter(jobdir):
if lease.expired():
lease.cleanup()
def _timed_out_jobs_queryset():
"""Return a QuerySet of timed out Jobs.
@returns: Django QuerySet
"""
models = autotest.load('frontend.afe.models')
return (
models.Job.objects
.filter(hostqueueentry__complete=False)
.extra(where=['created_on + INTERVAL timeout_mins MINUTE < NOW()'])
.distinct()
)
def _aborting_jobs_queryset():
"""Return a QuerySet of aborting Jobs.
@returns: Django QuerySet
"""
models = autotest.load('frontend.afe.models')
return (
models.Job.objects
.filter(hostqueueentry__aborted=True)
.filter(hostqueueentry__complete=False)
.distinct()
)
class _Metrics(object):
"""Class for sending job_aborter metrics."""
def __init__(self):
metrics = autotest.chromite_load('metrics')
prefix = 'chromeos/lucifer/job_aborter'
self._starting_m = metrics.Counter(prefix + '/start')
self._tick_m = metrics.Counter(prefix + '/tick')
self._expired_m = metrics.Counter(prefix + '/expired_jobs')
def send_starting(self):
"""Send starting metric."""
self._starting_m.increment()
def send_tick(self):
"""Send tick metric."""
self._tick_m.increment()
def send_expired_jobs(self, count):
"""Send expired_jobs metric."""
self._expired_m.increment_by(count)
if __name__ == '__main__':
main(sys.argv[1:])