Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

console_rules can exit with exit codes #6654

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions pants-plugins/src/python/internal_backend/rules_for_testing/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright 2018 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

python_library(
name = 'plugin',
sources = ['__init__.py', 'register.py'],
dependencies = [
'src/python/pants/base:exceptions',
'src/python/pants/engine:addressable',
'src/python/pants/engine:console',
'src/python/pants/engine:rules',
'src/python/pants/engine:selectors',
]
)
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# coding=utf-8
# Copyright 2018 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from __future__ import absolute_import, division, print_function, unicode_literals

from pants.engine.addressable import BuildFileAddresses
from pants.engine.console import Console
from pants.engine.rules import console_rule
from pants.engine.selectors import Select
from pants.rules.core.exceptions import GracefulTerminationException


@console_rule('list-and-die-for-testing', [Select(Console), Select(BuildFileAddresses)])
def fast_list_and_die_for_testing(console, addresses):
"""A fast variant of `./pants list` with a reduced feature set."""
for address in addresses.dependencies:
console.print_stdout(address.spec)
raise GracefulTerminationException(exit_code=42)


def rules():
return (fast_list_and_die_for_testing,)
1 change: 1 addition & 0 deletions pants.ini
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pythonpath: [

backend_packages: +[
"pants.backend.docgen",
"internal_backend.rules_for_testing",
"internal_backend.repositories",
"internal_backend.sitegen",
"internal_backend.utilities",
Expand Down
1 change: 1 addition & 0 deletions src/python/pants/bin/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ python_library(
'src/python/pants/base:build_environment',
'src/python/pants/base:build_file',
'src/python/pants/base:cmd_line_spec_parser',
'src/python/pants/base:exceptions',
'src/python/pants/base:exception_sink',
'src/python/pants/base:exiter',
'src/python/pants/base:project_tree',
Expand Down
5 changes: 4 additions & 1 deletion src/python/pants/bin/daemon_pants_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from pants.java.nailgun_io import NailgunStreamStdinReader, NailgunStreamWriter
from pants.java.nailgun_protocol import ChunkType, NailgunProtocol
from pants.pantsd.process_manager import ProcessManager
from pants.rules.core.exceptions import GracefulTerminationException
from pants.util.contextutil import hermetic_environment_as, stdio_as
from pants.util.socket import teardown_socket

Expand Down Expand Up @@ -230,10 +231,12 @@ def nailgunned_stdio(cls, sock, env, handle_stdin=True):
def _raise_deferred_exc(self):
"""Raises deferred exceptions from the daemon's synchronous path in the post-fork client."""
if self._deferred_exception:
exc_type, exc_value, exc_traceback = self._deferred_exception
if exc_type == GracefulTerminationException:
self._exiter.exit(exc_value.exit_code)
try:
# Expect `_deferred_exception` to be a 3-item tuple of the values returned by sys.exc_info().
# This permits use the 3-arg form of the `raise` statement to preserve the original traceback.
exc_type, exc_value, exc_traceback = self._deferred_exception
raise_with_traceback(exc_type(exc_value), exc_traceback)
except ValueError:
# If `_deferred_exception` isn't a 3-item tuple, treat it like a bare exception.
Expand Down
6 changes: 5 additions & 1 deletion src/python/pants/bin/local_pants_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from pants.init.target_roots_calculator import TargetRootsCalculator
from pants.option.options_bootstrapper import OptionsBootstrapper
from pants.reporting.reporting import Reporting
from pants.rules.core.exceptions import GracefulTerminationException
from pants.util.contextutil import maybe_profiled


Expand Down Expand Up @@ -190,8 +191,11 @@ def _maybe_run_v2(self):
self._options.goals_and_possible_v2_goals,
self._target_roots
)
except GracefulTerminationException as e:
logger.debug('Encountered graceful termination exception {}; exiting'.format(e))
return e.exit_code
except Exception as e:
logger.warn('Encountered unhandled exception {!r} during rule execution!'
logger.warn('Encountered unhandled exception {} during rule execution!'
.format(e))
return 1
else:
Expand Down
7 changes: 7 additions & 0 deletions src/python/pants/engine/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from pants.engine.nodes import Return, State, Throw
from pants.engine.rules import RuleIndex, SingletonRule, TaskRule
from pants.engine.selectors import Select, constraint_for
from pants.rules.core.exceptions import GracefulTerminationException
from pants.util.contextutil import temporary_file_path
from pants.util.dirutil import check_no_overlapping_paths
from pants.util.objects import Collection, datatype
Expand Down Expand Up @@ -536,6 +537,12 @@ def products_request(self, products, subjects):
throw_root_states = tuple(state for root, state in result.root_products if type(state) is Throw)
if throw_root_states:
unique_exceptions = tuple({t.exc for t in throw_root_states})

# TODO: consider adding a new top-level function adjacent to products_request used for running console tasks,
# so that this code doesn't need to exist in this form.
if len(unique_exceptions) == 1 and isinstance(unique_exceptions[0], GracefulTerminationException):
Copy link
Member

@stuhood stuhood Oct 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmmm... this bit of magic is unpleasant.

It almost makes me wonder whether we shouldn't have a separate/explicit Scheduler entrypoint for @console_rule? Would likely be useful for @wisechengyi's UX PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a TODO for now

raise unique_exceptions[0]

exception_noun = pluralize(len(unique_exceptions), 'Exception')

if self._scheduler.include_trace_on_error:
Expand Down
30 changes: 30 additions & 0 deletions src/python/pants/rules/core/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# coding=utf-8
# Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from __future__ import absolute_import, division, print_function, unicode_literals


class GracefulTerminationException(Exception):
"""Indicates that a console_rule should eagerly terminate the run.

No error trace will be printed if this is raised; the runner will simply exit with the passed
exit code.

Nothing except a console_rule should ever raise this.
"""

def __init__(self, message='', exit_code=1):
"""
:param int exit_code: an optional exit code (default=1)
"""
super(GracefulTerminationException, self).__init__(message)

if exit_code == 0:
raise ValueError("Cannot create GracefulTerminationException with exit code of 0")

self._exit_code = exit_code

@property
def exit_code(self):
return self._exit_code
15 changes: 14 additions & 1 deletion tests/python/pants_test/engine/test_scheduler_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import os

from pants.util.contextutil import temporary_dir
from pants_test.pants_run_integration_test import PantsRunIntegrationTest
from pants_test.pants_run_integration_test import PantsRunIntegrationTest, ensure_daemon


class SchedulerIntegrationTest(PantsRunIntegrationTest):
Expand All @@ -24,3 +24,16 @@ def test_visualize_to(self):
]
self.assert_success(self.run_pants(args))
self.assertTrue(len(os.listdir(destdir)) > 0)

@ensure_daemon
def test_graceful_termination(self):
args = [
'--no-v1',
'--v2',
'list-and-die-for-testing',
'examples/src/scala/org/pantsbuild/example/hello/welcome',
]
pants_result = self.run_pants(args)
self.assert_failure(pants_result)
self.assertEqual(pants_result.stdout_data, 'examples/src/scala/org/pantsbuild/example/hello/welcome:welcome\n')
self.assertEqual(pants_result.returncode, 42)
5 changes: 5 additions & 0 deletions tests/python/pants_test/pants_run_integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ def wrapper(self, *args, **kwargs):
with environment_as(**env):
try:
f(self, *args, **kwargs)
except Exception:
print('Test failed with enable-pantsd={}:'.format(enable_daemon))
if enable_daemon == 'false':
print('Skipping run with enable-pantsd=true because it already failed with enable-pantsd=false.')
raise
finally:
if enable_daemon:
self.assert_success(self.run_pants(['kill-pantsd']))
Expand Down