blob: 23fdd2b7506f21c48554b27112f7b40f59900c7b [file] [log] [blame] [edit]
# Copyright 2014 The LUCI Authors. All rights reserved.
# Use of this source code is governed under the Apache License, Version 2.0
# that can be found in the LICENSE file.
"""Main entry point for Swarming backend handlers."""
import datetime
import json
import logging
import webapp2
from google.appengine.api import datastore_errors
from google.appengine.ext import ndb
from google.appengine import runtime
from google.protobuf import json_format
from proto.plugin import plugin_pb2
from components import decorators
from components import datastore_utils
from components import utils
from server import bot_groups_config
from server import bot_management
from server import config
from server import external_scheduler
from server import named_caches
from server import task_queues
from server import task_result
from server import task_scheduler
import ts_mon_metrics
class WarmupHandler(webapp2.RequestHandler):
def get(self):
self.response.headers['Content-Type'] = 'text/plain; charset=utf-8'
self.response.write('ok')
## Cron jobs.
class _CronHandlerBase(webapp2.RequestHandler):
@decorators.require_cronjob
def get(self):
self.run_cron()
def run_cron(self):
raise NotImplementedError()
class CronBotDiedHandler(_CronHandlerBase):
"""Sets running tasks where the bot is not sending ping updates for several
minutes as BOT_DIED.
"""
def run_cron(self):
task_scheduler.cron_handle_bot_died()
class CronAbortExpiredShardToRunHandler(_CronHandlerBase):
"""Set tasks that haven't started before their expiration_ts timestamp as
EXPIRED.
Most of the tasks will be expired 'inline' as bots churn through the queue,
but tasks where the bots are not polling will be expired by this cron job.
"""
def run_cron(self):
task_scheduler.cron_abort_expired_task_to_run()
class CronTidyTaskDimensionSets(_CronHandlerBase):
"""Removes expired task dimension sets from the datastore."""
def run_cron(self):
f = task_queues.tidy_task_dimension_sets_async()
if not f.get_result():
self.response.set_status(429, 'Need to retry')
class CronTidyBotDimensionsMatches(_CronHandlerBase):
"""Removes stale BotDimensionsMatches from the datastore."""
def run_cron(self):
f = task_queues.tidy_bot_dimensions_matches_async(
bot_management.check_bot_alive_async)
if not f.get_result():
self.response.set_status(429, 'Need to retry')
class CronUpdateBotInfoComposite(_CronHandlerBase):
"""Updates BotInfo.composite if needed, e.g. the bot became dead because it
hasn't pinged for a while.
"""
def run_cron(self):
bot_management.cron_update_bot_info()
class CronNamedCachesUpdate(_CronHandlerBase):
"""Updates named caches hints."""
def run_cron(self):
named_caches.cron_update_named_caches()
class CronBotGroupsConfigHandler(_CronHandlerBase):
"""Fetches bots.cfg with all includes, assembles the final config."""
def run_cron(self):
try:
bot_groups_config.refetch_from_config_service()
except bot_groups_config.BadConfigError:
pass
class CronExternalSchedulerCancellationsHandler(_CronHandlerBase):
"""Fetches cancelled tasks from external schedulers, and cancels them."""
def run_cron(self):
task_scheduler.cron_handle_external_cancellations()
class CronExternalSchedulerGetCallbacksHandler(_CronHandlerBase):
"""Fetches callbacks requests from external schedulers, and notifies them."""
def run_cron(self):
task_scheduler.cron_handle_get_callbacks()
## Task queues.
class TaskCancelTasksHandler(webapp2.RequestHandler):
"""Cancels tasks given a list of their ids."""
@decorators.silence(datastore_utils.CommitError)
@decorators.require_taskqueue('cancel-tasks')
def post(self):
payload = json.loads(self.request.body)
logging.info('Cancelling tasks with ids: %s', payload['tasks'])
kill_running = payload['kill_running']
# TODO(maruel): Parallelize.
for task_id in payload['tasks']:
ok, was_running = task_scheduler.cancel_task_with_id(
task_id, kill_running, None)
logging.info('task %s canceled: %s was running: %s',
task_id, ok, was_running)
class TaskCancelTaskOnBotHandler(webapp2.RequestHandler):
"""Cancels a given task if it is running on the given bot.
If bot is not specified, cancel task unconditionally.
If bot is specified, and task is not running on bot, then do nothing.
"""
@decorators.require_taskqueue('cancel-task-on-bot')
def post(self):
payload = json.loads(self.request.body)
task_id = payload.get('task_id')
if not task_id:
logging.error('Missing task_id.')
return
bot_id = payload.get('bot_id')
try:
ok, was_running = task_scheduler.cancel_task_with_id(
task_id, True, bot_id)
logging.info('task %s canceled: %s was running: %s',
task_id, ok, was_running)
except ValueError:
# Ignore errors that may be due to missing or invalid tasks.
logging.warning('Ignoring a task cancellation due to exception.',
exc_info=True)
class TaskCancelChildrenTasksHandler(webapp2.RequestHandler):
"""Cancels children tasks with pending state of the given task."""
@decorators.silence(runtime.DeadlineExceededError)
@decorators.require_taskqueue('cancel-children-tasks')
def post(self):
payload = json.loads(self.request.body)
task = payload['task']
logging.info('Cancelling children tasks of task %s', task)
task_scheduler.task_cancel_running_children_tasks(task)
class TaskExpireTasksHandler(webapp2.RequestHandler):
"""Expires a list of tasks, given a list of their ids."""
@decorators.require_taskqueue('task-expire')
def post(self):
payload = json.loads(self.request.body)
task_scheduler.task_expire_tasks(payload['entities'])
class TaskUpdateBotMatchesHandler(webapp2.RequestHandler):
"""Assigns new task queues to existing bots."""
@decorators.require_taskqueue('update-bot-matches')
def post(self):
f = task_queues.update_bot_matches_async(self.request.body)
if not f.get_result():
self.response.set_status(429, 'Need to retry')
class TaskRescanMatchingTaskSetsHandler(webapp2.RequestHandler):
"""A task queue task that finds all matching TaskDimensionsSets for a bot."""
@decorators.require_taskqueue('rescan-matching-task-sets')
def post(self):
f = task_queues.rescan_matching_task_sets_async(self.request.body)
if not f.get_result():
self.response.set_status(429, 'Need to retry')
class TaskSendPubSubMessage(webapp2.RequestHandler):
"""Sends PubSub notification about task completion."""
# Add task_id to the URL for better visibility in request logs.
@decorators.require_taskqueue('pubsub')
def post(self, task_id): # pylint: disable=unused-argument
task_scheduler.task_handle_pubsub_task(json.loads(self.request.body))
class TaskNotifyBuildbucketHandler(webapp2.RequestHandler):
"""Sends updates to Buildbucket about task status."""
@decorators.require_taskqueue('buildbucket-notify')
def post(self, task_id): # pylint: disable=unused-argument
task_scheduler.task_buildbucket_update(json.loads(self.request.body))
class TaskESNotifyTasksHandler(webapp2.RequestHandler):
"""Sends task notifications to external scheduler."""
@decorators.require_taskqueue('es-notify-tasks')
def post(self):
es_host = self.request.get('es_host')
request_json = self.request.get('request_json')
request = plugin_pb2.NotifyTasksRequest()
json_format.Parse(request_json, request)
external_scheduler.notify_request_now(es_host, request)
class TaskESNotifyKickHandler(webapp2.RequestHandler):
"""Kicks off the pull queue worker to batch the es-notifications."""
@decorators.require_taskqueue('es-notify-kick')
def post(self):
external_scheduler.task_batch_handle_notifications()
class TaskNamedCachesPool(webapp2.RequestHandler):
"""Update named caches cache for a pool."""
@decorators.silence(datastore_errors.Timeout)
@decorators.require_taskqueue('named-cache-task')
def post(self):
params = json.loads(self.request.body)
logging.info('Handling pool: %s', params['pool'])
named_caches.task_update_pool(params['pool'])
###
def get_routes():
"""Returns internal urls that should only be accessible via the backend."""
routes = [
('/_ah/warmup', WarmupHandler),
# Cron jobs.
('/internal/cron/important/scheduler/abort_bot_missing',
CronBotDiedHandler),
('/internal/cron/important/scheduler/abort_expired',
CronAbortExpiredShardToRunHandler),
('/internal/cron/cleanup/task_dimension_sets', CronTidyTaskDimensionSets),
('/internal/cron/cleanup/bot_dimensions_matches',
CronTidyBotDimensionsMatches),
('/internal/cron/monitoring/bots/update_bot_info',
CronUpdateBotInfoComposite),
('/internal/cron/important/bot_groups_config',
CronBotGroupsConfigHandler),
('/internal/cron/important/external_scheduler/cancellations',
CronExternalSchedulerCancellationsHandler),
('/internal/cron/important/external_scheduler/get_callbacks',
CronExternalSchedulerGetCallbacksHandler),
('/internal/cron/important/named_caches/update', CronNamedCachesUpdate),
# Task queues.
('/internal/taskqueue/important/tasks/cancel', TaskCancelTasksHandler),
('/internal/taskqueue/important/tasks/cancel-task-on-bot',
TaskCancelTaskOnBotHandler),
('/internal/taskqueue/important/tasks/cancel-children-tasks',
TaskCancelChildrenTasksHandler),
('/internal/taskqueue/important/tasks/expire', TaskExpireTasksHandler),
('/internal/taskqueue/important/task_queues/update-bot-matches',
TaskUpdateBotMatchesHandler),
('/internal/taskqueue/important/task_queues/rescan-matching-task-sets',
TaskRescanMatchingTaskSetsHandler),
(r'/internal/taskqueue/important/pubsub/notify-task/<task_id:[0-9a-f]+>',
TaskSendPubSubMessage),
(r'/internal/taskqueue/important/buildbucket/notify-task/'
r'<task_id:[0-9a-f]+>', TaskNotifyBuildbucketHandler),
('/internal/taskqueue/important/external_scheduler/notify-tasks',
TaskESNotifyTasksHandler),
('/internal/taskqueue/important/external_scheduler/notify-kick',
TaskESNotifyKickHandler),
(r'/internal/taskqueue/important/named_cache/update-pool',
TaskNamedCachesPool),
]
return [webapp2.Route(*a) for a in routes]
def create_application(debug):
return webapp2.WSGIApplication(get_routes(), debug=debug)