From ba8e8c053506ab05693bd70253c965f702d3a0c9 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Fri, 1 May 2020 18:55:04 +0200 Subject: [PATCH] Example about how to kill a running task --- readthedocs/builds/models.py | 6 +++ readthedocs/builds/views.py | 16 ++++++++ readthedocs/core/utils/__init__.py | 5 ++- readthedocs/doc_builder/environments.py | 10 ++++- readthedocs/doc_builder/exceptions.py | 4 ++ readthedocs/projects/tasks.py | 39 +++++++++++++++++++ .../templates/builds/build_detail.html | 6 +++ 7 files changed, 84 insertions(+), 2 deletions(-) diff --git a/readthedocs/builds/models.py b/readthedocs/builds/models.py index 63bbbf0b81b..0572da4d78a 100644 --- a/readthedocs/builds/models.py +++ b/readthedocs/builds/models.py @@ -687,6 +687,12 @@ class Build(models.Model): _('Cold Storage'), help_text='Build steps stored outside the database.', ) + task_id = models.CharField( + _('Celery task id'), + max_length=36, + null=True, + blank=True, + ) # Managers objects = BuildManager.from_queryset(BuildQuerySet)() diff --git a/readthedocs/builds/views.py b/readthedocs/builds/views.py index 3167570752f..6f7fda817a2 100644 --- a/readthedocs/builds/views.py +++ b/readthedocs/builds/views.py @@ -153,3 +153,19 @@ def get_context_data(self, **kwargs): issue_url = urlparse(issue_url).geturl() context['issue_url'] = issue_url return context + + @method_decorator(login_required) + def post(self, request, project_slug, build_pk): + project = get_object_or_404(Project, slug=project_slug) + build = get_object_or_404(Build, pk=build_pk) + + if not AdminPermission.is_admin(request.user, project): + return HttpResponseForbidden() + + import signal + from celery.task.control import revoke + revoke(build.task_id, signal=signal.SIGINT, terminate=True) + + return HttpResponseRedirect( + reverse('builds_detail', args=[project.slug, build.pk]), + ) diff --git a/readthedocs/core/utils/__init__.py b/readthedocs/core/utils/__init__.py index 693d241656d..468a97ab352 100644 --- a/readthedocs/core/utils/__init__.py +++ b/readthedocs/core/utils/__init__.py @@ -225,7 +225,10 @@ def trigger_build(project, version=None, commit=None, record=True, force=False): # Build was skipped return (None, None) - return (update_docs_task.apply_async(), build) + task = update_docs_task.apply_async() + build.task_id = task.id + build.save() + return task, build def send_email( diff --git a/readthedocs/doc_builder/environments.py b/readthedocs/doc_builder/environments.py index 2efe46d667a..dd6cd4ec4f1 100644 --- a/readthedocs/doc_builder/environments.py +++ b/readthedocs/doc_builder/environments.py @@ -50,6 +50,7 @@ ProjectBuildsSkippedError, VersionLockedError, YAMLParseError, + BuildKilled, ) @@ -356,7 +357,7 @@ def run(self): '\n\nCommand killed due to excessive memory consumption\n', ), ) - except DockerAPIError: + except (DockerAPIError, BuildKilled): self.exit_code = -1 if self.output is None or not self.output: self.output = _('Command exited abnormally') @@ -605,6 +606,13 @@ def handle_exception(self, exc_type, exc_value, _): elif exc_type in self.WARNING_EXCEPTIONS: log_level_function = log.warning self.failure = exc_value + elif exc_type is BuildKilled: + log.warning('KILLED FROM handle_exception') + log_level_function = log.warning + self.failure = 'Killed!' + self.build['state'] = BUILD_STATE_FINISHED + # self.done = True + # self.successful = False else: log_level_function = log.error self.failure = exc_value diff --git a/readthedocs/doc_builder/exceptions.py b/readthedocs/doc_builder/exceptions.py index d7a511430a4..8c7e3705037 100644 --- a/readthedocs/doc_builder/exceptions.py +++ b/readthedocs/doc_builder/exceptions.py @@ -90,3 +90,7 @@ class MkDocsYAMLParseError(BuildEnvironmentError): 'Please follow the user guide https://www.mkdocs.org/user-guide/configuration/ ' 'to configure the file properly.', ) + + +class BuildKilled(Exception): + pass diff --git a/readthedocs/projects/tasks.py b/readthedocs/projects/tasks.py index 963a3692f99..1d8533c39c7 100644 --- a/readthedocs/projects/tasks.py +++ b/readthedocs/projects/tasks.py @@ -64,6 +64,7 @@ ProjectBuildsSkippedError, VersionLockedError, YAMLParseError, + BuildKilled, ) from readthedocs.doc_builder.loader import get_builder_class from readthedocs.doc_builder.python_environments import Conda, Virtualenv @@ -536,7 +537,32 @@ def run( :rtype: bool """ + try: + def sigstop_received(*args, **kwargs): + log.warning('SIGSTOP received. Killing the current building.') + env_cls = DockerBuildEnvironment + # environment = env_cls( + # project=self.project, + # version=self.version, + # build=self.build, + # record=False, + # update_on_success=False, + # ) + # client = environment.get_client() + # client.kill(environment.container_id) + # client.remove_container(environment.container_id) + # api_v2.build(self.build['id']).patch({ + # 'error': 'Killed!', + # 'success': False, + # 'state': BUILD_STATE_FINISHED, + # }) + raise BuildKilled + return False + + # signal.signal(signal.SIGUSR2, sigstop_received) + signal.signal(signal.SIGINT, sigstop_received) + self.version = self.get_version(version_pk) self.project = self.version.project self.build = self.get_build(build_pk) @@ -585,6 +611,19 @@ def run( return False self.run_build(record=record) return True + except BuildKilled: + log.warning( + 'Build killed by user. project=%s version=%s build=%s', + self.project.slug, + self.version.slug, + build_pk, + ) + api_v2.build(self.build['id']).patch({ + 'error': 'Killed!', + 'success': False, + 'state': BUILD_STATE_FINISHED, + }) + return False except Exception: log.exception( 'An unhandled exception was raised during build setup', diff --git a/readthedocs/templates/builds/build_detail.html b/readthedocs/templates/builds/build_detail.html index 4124600f3ed..8f84fd70752 100644 --- a/readthedocs/templates/builds/build_detail.html +++ b/readthedocs/templates/builds/build_detail.html @@ -30,6 +30,12 @@ {% block content %}
+
+ {% csrf_token %} + +
+ +