Adding in cancel for a build that is building.
This commit is contained in:
parent
e57eece6c1
commit
fd7c566d31
7 changed files with 120 additions and 16 deletions
2
app.py
2
app.py
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
25
buildman/manager/buildcanceller.py
Normal file
25
buildman/manager/buildcanceller.py
Normal 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)
|
37
buildman/manager/etcd_canceller.py
Normal file
37
buildman/manager/etcd_canceller.py
Normal 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
|
8
buildman/manager/noop_canceller.py
Normal file
8
buildman/manager/noop_canceller.py
Normal 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
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
Reference in a new issue