Adding in cancel for a build that is building.

This commit is contained in:
Charlton Austin 2016-10-27 13:18:02 -04:00
parent e57eece6c1
commit fd7c566d31
7 changed files with 120 additions and 16 deletions

2
app.py
View file

@ -15,6 +15,7 @@ from werkzeug.routing import BaseConverter
import features import features
from avatars.avatars import Avatar from avatars.avatars import Avatar
from buildman.manager.buildcanceller import BuildCanceller
from data import database from data import database
from data import model from data import model
from data.archivedlogs import LogArchive from data.archivedlogs import LogArchive
@ -192,6 +193,7 @@ superusers = SuperUserManager(app)
signer = Signer(app, config_provider) signer = Signer(app, config_provider)
instance_keys = InstanceKeys(app) instance_keys = InstanceKeys(app)
label_validator = LabelValidator(app) label_validator = LabelValidator(app)
build_canceller = BuildCanceller(app)
license_validator = LicenseValidator(config_provider) license_validator = LicenseValidator(config_provider)
license_validator.start() license_validator.start()

View file

@ -246,6 +246,10 @@ class BuildComponent(BaseComponent):
try: try:
if self._build_status.set_phase(phase, log_data.get('status_data')): if self._build_status.set_phase(phase, log_data.get('status_data')):
logger.debug('Build %s has entered a new phase: %s', self.builder_realm, phase) logger.debug('Build %s has entered a new phase: %s', self.builder_realm, phase)
elif self._current_job.repo_build.phase == BUILD_PHASE.CANCELLED:
build_id = self._current_job.repo_build.uuid
logger.debug('Trying to move cancelled build into phase: %s with id: %s', phase, build_id)
return False
except InvalidRepositoryBuildException: except InvalidRepositoryBuildException:
build_id = self._current_job.repo_build.uuid build_id = self._current_job.repo_build.uuid
logger.info('Build %s was not found; repo was probably deleted', build_id) logger.info('Build %s was not found; repo was probably deleted', build_id)

View file

@ -0,0 +1,25 @@
import logging
from buildman.manager.etcd_canceller import EtcdCanceller
from buildman.manager.noop_canceller import NoopCanceller
logger = logging.getLogger(__name__)
CANCELLERS = {'ephemeral': EtcdCanceller}
class BuildCanceller(object):
""" A class to manage cancelling a build """
def __init__(self, app=None):
build_manager_config = app.config.get('BUILD_MANAGER')
if app is None or build_manager_config is None:
self.handler = NoopCanceller()
return
canceller = CANCELLERS.get(build_manager_config[0], NoopCanceller)
self.handler = canceller(build_manager_config[1])
def try_cancel_build(self, uuid):
""" A method to kill a running build """
return self.handler.try_cancel_build(uuid)

View file

@ -0,0 +1,37 @@
import logging
import etcd
logger = logging.getLogger(__name__)
class EtcdCanceller(object):
""" A class that sends a message to etcd to cancel a build """
def __init__(self, config):
etcd_host = config.get('ETCD_HOST', '127.0.0.1')
etcd_port = config.get('ETCD_PORT', 2379)
etcd_ca_cert = config.get('ETCD_CA_CERT', None)
etcd_auth = config.get('ETCD_CERT_AND_KEY', None)
if etcd_auth is not None:
etcd_auth = tuple(etcd_auth)
etcd_protocol = 'http' if etcd_auth is None else 'https'
logger.debug('Connecting to etcd on %s:%s', etcd_host, etcd_port)
self._cancel_prefix = config.get('ETCD_CANCEL_PREFIX', 'cancel/')
self._etcd_client = etcd.Client(
host=etcd_host,
port=etcd_port,
cert=etcd_auth,
ca_cert=etcd_ca_cert,
protocol=etcd_protocol,
read_timeout=5)
def try_cancel_build(self, build_uuid):
""" Writes etcd message to cancel build_uuid. """
logger.info("Cancelling build %s".format(build_uuid))
try:
self._etcd_client.write("{}{}".format(self._cancel_prefix, build_uuid), build_uuid)
return True
except etcd.EtcdException:
logger.exception("Failed to write to etcd client %s", build_uuid)
return False

View file

@ -0,0 +1,8 @@
class NoopCanceller(object):
""" A class that can not cancel a build """
def __init__(self, config=None):
pass
def try_cancel_build(self, uuid):
""" Does nothing and fails to cancel build. """
return False

View file

@ -741,6 +741,7 @@ class BUILD_PHASE(object):
PUSHING = 'pushing' PUSHING = 'pushing'
WAITING = 'waiting' WAITING = 'waiting'
COMPLETE = 'complete' COMPLETE = 'complete'
CANCELLED = 'cancelled'
@classmethod @classmethod
def is_terminal_phase(cls, phase): def is_terminal_phase(cls, phase):

View file

@ -10,6 +10,8 @@ from data.model import (InvalidBuildTriggerException, InvalidRepositoryBuildExce
PRESUMED_DEAD_BUILD_AGE = timedelta(days=15) PRESUMED_DEAD_BUILD_AGE = timedelta(days=15)
PHASES_NOT_ALLOWED_TO_CANCEL_FROM = (BUILD_PHASE.PUSHING, BUILD_PHASE.COMPLETE,
BUILD_PHASE.ERROR, BUILD_PHASE.INTERNAL_ERROR)
def update_build_trigger(trigger, config, auth_token=None): def update_build_trigger(trigger, config, auth_token=None):
@ -143,54 +145,79 @@ def get_pull_robot_name(trigger):
return trigger.pull_robot.username return trigger.pull_robot.username
def _get_build_row_for_update(build_uuid):
return db_for_update(RepositoryBuild.select().where(RepositoryBuild.uuid == build_uuid)).get()
def update_phase(build_uuid, phase): def update_phase(build_uuid, phase):
""" A function to change the phase of a build """ """ A function to change the phase of a build """
with db_transaction(): try:
try: build = _get_build_row_for_update(build_uuid)
build = get_repository_build(build_uuid) except RepositoryBuild.DoesNotExist:
build.phase = phase
build.save()
return True
except InvalidRepositoryBuildException:
return False return False
# Can't update a cancelled build
if build.phase == BUILD_PHASE.CANCELLED:
return False
build.phase = phase
build.save()
return True
def create_cancel_build_in_queue(build, build_queue): def create_cancel_build_in_queue(build, build_queue):
""" A function to cancel a build before it leaves the queue """ """ A function to cancel a build before it leaves the queue """
def cancel_build(): def cancel_build():
if build.phase != BUILD_PHASE.WAITING or not build.queue_id: cancelled = False
if build.queue_id is not None:
cancelled = build_queue.cancel(build.queue_id)
if build.phase != BUILD_PHASE.WAITING:
return False return False
cancelled = build_queue.cancel(build.queue_id)
if cancelled:
# Delete the build row.
build.delete_instance()
return cancelled return cancelled
return cancel_build return cancel_build
def create_cancel_build_in_manager(build): def create_cancel_build_in_manager(build, build_canceller):
""" A function to cancel the build before it starts to push """ """ A function to cancel the build before it starts to push """
def cancel_build(): def cancel_build():
return False original_phase = build.phase
if build.phase in PHASES_NOT_ALLOWED_TO_CANCEL_FROM:
return False
build.phase = BUILD_PHASE.CANCELLED
build.save()
if not build_canceller.try_cancel_build(build.uuid):
build.phase = original_phase
build.save()
return False
return True
return cancel_build return cancel_build
def cancel_repository_build(build, build_queue): def cancel_repository_build(build, build_queue):
""" This tries to cancel the build returns true if request is successful false if it can't be cancelled """ """ This tries to cancel the build returns true if request is successful false if it can't be cancelled """
with db_transaction(): with db_transaction():
from app import build_canceller
# Reload the build for update. # Reload the build for update.
# We are loading the build for update so checks should be as quick as possible. # We are loading the build for update so checks should be as quick as possible.
try: try:
build = db_for_update(RepositoryBuild.select().where(RepositoryBuild.id == build.id)).get() build = _get_build_row_for_update(build.uuid)
except RepositoryBuild.DoesNotExist: except RepositoryBuild.DoesNotExist:
return False return False
cancel_builds = [create_cancel_build_in_queue(build, build_queue), cancel_builds = [create_cancel_build_in_queue(build, build_queue),
create_cancel_build_in_manager(build), ] create_cancel_build_in_manager(build, build_canceller), ]
for cancelled in cancel_builds: for cancelled in cancel_builds:
if cancelled(): if cancelled():
# Delete the build row.
# TODO Charlie 2016-11-11 Add in message that says build was cancelled and remove the delete build.
build.delete_instance()
return True return True
return False return False