You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@aurora.apache.org by mc...@apache.org on 2014/06/06 16:19:42 UTC
git commit: Make errors in the thrift API interface handle-able by
the aurora client.
Repository: incubator-aurora
Updated Branches:
refs/heads/master 36cc66628 -> 274b185c5
Make errors in the thrift API interface handle-able by the aurora client.
- Add a general exception catch around the code that calls thrift RPCs,
so that any error that occurs in the thrift code will be turned
into a ThriftInternalException.
- Modify the client so that it catches internal errors without dumping
stack, and cleanly exits with EXIT_API_ERROR.
- Add a file for tests of API functionality, instead of co-opting
existing tests.
Reviewed at https://reviews.apache.org/r/22274/
Project: http://git-wip-us.apache.org/repos/asf/incubator-aurora/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-aurora/commit/274b185c
Tree: http://git-wip-us.apache.org/repos/asf/incubator-aurora/tree/274b185c
Diff: http://git-wip-us.apache.org/repos/asf/incubator-aurora/diff/274b185c
Branch: refs/heads/master
Commit: 274b185c536d7e9b5d837a0c67795bb683042b46
Parents: 36cc666
Author: Mark Chu-Carroll <mc...@twopensource.com>
Authored: Fri Jun 6 10:11:55 2014 -0400
Committer: Mark Chu-Carroll <mc...@twitter.com>
Committed: Fri Jun 6 10:18:46 2014 -0400
----------------------------------------------------------------------
.../python/apache/aurora/client/api/__init__.py | 6 +-
.../aurora/client/api/scheduler_client.py | 6 +
.../python/apache/aurora/client/cli/__init__.py | 3 +
src/test/python/apache/aurora/client/cli/BUILD | 15 ++
.../aurora/client/cli/test_api_from_cli.py | 143 +++++++++++++++++++
5 files changed, 172 insertions(+), 1 deletion(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/274b185c/src/main/python/apache/aurora/client/api/__init__.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/api/__init__.py b/src/main/python/apache/aurora/client/api/__init__.py
index a73f23f..99444d6 100644
--- a/src/main/python/apache/aurora/client/api/__init__.py
+++ b/src/main/python/apache/aurora/client/api/__init__.py
@@ -33,6 +33,7 @@ class AuroraClientAPI(object):
class Error(Exception): pass
class TypeError(Error, TypeError): pass
class ClusterMismatch(Error, ValueError): pass
+ class ThriftInternalError(Error): pass
def __init__(self, cluster, verbose=False, session_key_factory=make_session_key):
if not isinstance(cluster, Cluster):
@@ -109,7 +110,10 @@ class AuroraClientAPI(object):
environment=env)
def query(self, query):
- return self._scheduler_proxy.getTasksStatus(query)
+ try:
+ return self._scheduler_proxy.getTasksStatus(query)
+ except SchedulerProxy.ThriftInternalError as e:
+ raise self.ThriftInternalError(e.args[0])
def update_job(self, config, health_check_interval_seconds=3, instances=None):
"""Run a job update for a given config, for the specified instances. If
http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/274b185c/src/main/python/apache/aurora/client/api/scheduler_client.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/api/scheduler_client.py b/src/main/python/apache/aurora/client/api/scheduler_client.py
index 86ccdd9..7c5fba6 100644
--- a/src/main/python/apache/aurora/client/api/scheduler_client.py
+++ b/src/main/python/apache/aurora/client/api/scheduler_client.py
@@ -182,6 +182,7 @@ class SchedulerProxy(object):
class TimeoutError(Error): pass
class AuthenticationError(Error): pass
class APIVersionError(Error): pass
+ class ThriftInternalError(Error): pass
def __init__(self, cluster, verbose=False, session_key_factory=make_session_key):
"""A callable session_key_factory should be provided for authentication"""
@@ -267,6 +268,11 @@ class SchedulerProxy(object):
log.warning('Connection error with scheduler: %s, reconnecting...' % e)
self.invalidate()
time.sleep(self.RPC_RETRY_INTERVAL.as_(Time.SECONDS))
+ except Exception as e:
+ # Take any error that occurs during the RPC call, and transform it
+ # into something clients can handle.
+ raise self.ThriftInternalError("Error during thrift call %s to %s: %s" %
+ (method_name, self.cluster.name, e))
raise self.TimeoutError('Timed out attempting to issue %s to %s' % (
method_name, self.cluster.name))
http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/274b185c/src/main/python/apache/aurora/client/cli/__init__.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/cli/__init__.py b/src/main/python/apache/aurora/client/cli/__init__.py
index fc63dbf..552f9f1 100644
--- a/src/main/python/apache/aurora/client/cli/__init__.py
+++ b/src/main/python/apache/aurora/client/cli/__init__.py
@@ -394,6 +394,9 @@ class CommandLine(object):
print_aurora_log(logging.INFO, "Error executing command: %s", c.msg)
self.print_err("Error executing command: %s" % c.msg)
return c.code
+ except Exception as e:
+ print_aurora_log(logging.ERROR, "Internal error executing command: %s", e)
+ return EXIT_API_ERROR
class Noun(AuroraCommand):
http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/274b185c/src/test/python/apache/aurora/client/cli/BUILD
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/client/cli/BUILD b/src/test/python/apache/aurora/client/cli/BUILD
index c91ae42..3c88ed7 100644
--- a/src/test/python/apache/aurora/client/cli/BUILD
+++ b/src/test/python/apache/aurora/client/cli/BUILD
@@ -15,6 +15,7 @@
python_test_suite(
name = 'all',
dependencies = [
+ pants(':api'),
pants(':bridge'),
pants(':command_hooks'),
pants(':cron'),
@@ -83,6 +84,20 @@ python_tests(
)
python_tests(
+ name = 'api',
+ sources = [ 'test_api_from_cli.py' ],
+ dependencies = [
+ pants(':util'),
+ pants('3rdparty/python:mock'),
+ pants('3rdparty/python:twitter.common.contextutil'),
+ pants('src/main/python/apache/aurora/client/cli'),
+ pants('src/main/python/apache/aurora/client/cli:client_lib'),
+ pants('src/test/python/apache/aurora/client/commands:util')
+ ]
+)
+
+
+python_tests(
name = 'config',
sources = [ 'test_config_noun.py' ],
dependencies = [
http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/274b185c/src/test/python/apache/aurora/client/cli/test_api_from_cli.py
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/client/cli/test_api_from_cli.py b/src/test/python/apache/aurora/client/cli/test_api_from_cli.py
new file mode 100644
index 0000000..bf72383
--- /dev/null
+++ b/src/test/python/apache/aurora/client/cli/test_api_from_cli.py
@@ -0,0 +1,143 @@
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import contextlib
+
+from mock import Mock, patch
+
+from apache.aurora.client.cli import EXIT_API_ERROR
+from apache.aurora.client.cli.client import AuroraCommandLine
+from apache.aurora.client.cli.util import AuroraClientCommandTest
+
+from gen.apache.aurora.api.ttypes import (
+ AssignedTask,
+ Identity,
+ JobKey,
+ ResponseCode,
+ ScheduleStatus,
+ ScheduleStatusResult,
+ TaskConfig,
+ TaskEvent,
+ TaskQuery
+)
+
+
+class TestApiFromCLI(AuroraClientCommandTest):
+ """A container for tests that are probing at API functionality,
+ to see if the CLI can handle API-level errors.
+ """
+
+ @classmethod
+ def create_mock_scheduled_tasks(cls):
+ jobs = []
+ for name in ['foo', 'bar', 'baz']:
+ job = Mock()
+ job.key = JobKey(role=cls.TEST_ROLE, environment=cls.TEST_ENV, name=name)
+ job.failure_count = 0
+ job.assignedTask = Mock(spec=AssignedTask)
+ job.assignedTask.slaveHost = 'slavehost'
+ job.assignedTask.task = Mock(spec=TaskConfig)
+ job.assignedTask.task.maxTaskFailures = 1
+ job.assignedTask.task.metadata = []
+ job.assignedTask.task.owner = Identity(role='bozo')
+ job.assignedTask.task.environment = 'test'
+ job.assignedTask.task.jobName = 'woops'
+ job.assignedTask.task.numCpus = 2
+ job.assignedTask.task.ramMb = 2
+ job.assignedTask.task.diskMb = 2
+ job.assignedTask.instanceId = 4237894
+ job.assignedTask.assignedPorts = None
+ job.status = ScheduleStatus.RUNNING
+ mockEvent = Mock(spec=TaskEvent)
+ mockEvent.timestamp = 28234726395
+ mockEvent.status = ScheduleStatus.RUNNING
+ mockEvent.message = "Hi there"
+ job.taskEvents = [mockEvent]
+ jobs.append(job)
+ return jobs
+
+ @classmethod
+ def create_mock_scheduled_task_no_metadata(cls):
+ result = cls.create_mock_scheduled_tasks()
+ for job in result:
+ job.assignedTask.task.metadata = None
+ return result
+
+ @classmethod
+ def create_getjobs_response(cls):
+ result = Mock()
+ result.responseCode = ResponseCode.OK
+ result.result = Mock()
+ result.result.getJobsResult = Mock()
+ mock_job_one = Mock()
+ mock_job_one.key = Mock()
+ mock_job_one.key.role = 'RoleA'
+ mock_job_one.key.environment = 'test'
+ mock_job_one.key.name = 'hithere'
+ mock_job_two = Mock()
+ mock_job_two.key = Mock()
+ mock_job_two.key.role = 'bozo'
+ mock_job_two.key.environment = 'test'
+ mock_job_two.key.name = 'hello'
+ result.result.getJobsResult.configs = [mock_job_one, mock_job_two]
+ return result
+
+ @classmethod
+ def create_status_response(cls):
+ resp = cls.create_simple_success_response()
+ resp.result.scheduleStatusResult = Mock(spec=ScheduleStatusResult)
+ resp.result.scheduleStatusResult.tasks = set(cls.create_mock_scheduled_tasks())
+ return resp
+
+ @classmethod
+ def create_status_response_null_metadata(cls):
+ resp = cls.create_simple_success_response()
+ resp.result.scheduleStatusResult = Mock(spec=ScheduleStatusResult)
+ resp.result.scheduleStatusResult.tasks = set(cls.create_mock_scheduled_task_no_metadata())
+ return resp
+
+ @classmethod
+ def create_failed_status_response(cls):
+ return cls.create_blank_response(ResponseCode.INVALID_REQUEST, 'No tasks found for query')
+
+ def test_successful_status_deep(self):
+ """Test the status command more deeply: in a request with a fully specified
+ job, it should end up doing a query using getTasksStatus."""
+ (mock_api, mock_scheduler_proxy) = self.create_mock_api()
+ mock_scheduler_proxy.query.return_value = self.create_status_response()
+ with contextlib.nested(
+ patch('apache.aurora.client.api.SchedulerProxy', return_value=mock_scheduler_proxy),
+ patch('apache.aurora.client.factory.CLUSTERS', new=self.TEST_CLUSTERS)):
+ cmd = AuroraCommandLine()
+ cmd.execute(['job', 'status', 'west/bozo/test/hello'])
+ mock_scheduler_proxy.getTasksStatus.assert_called_with(TaskQuery(jobName='hello',
+ environment='test', owner=Identity(role='bozo')))
+
+ def test_status_api_failure(self):
+ # Following should use spec=SchedulerClient, but due to introspection for the RPC calls,
+ # the necessary methods aren't in that spec.
+ mock_scheduler_client = Mock()
+ mock_scheduler_client.getTasksStatus.side_effect = IOError("Uh-Oh")
+ with contextlib.nested(
+ patch('apache.aurora.client.api.scheduler_client.SchedulerClient.get',
+ return_value=mock_scheduler_client),
+ patch('apache.aurora.client.factory.CLUSTERS', new=self.TEST_CLUSTERS)):
+
+ cmd = AuroraCommandLine()
+ # This should create a scheduler client, set everything up, and then issue a
+ # getTasksStatus call against the mock_scheduler_client. That should raise an
+ # exception, which results in the command failing with an error code.
+ result = cmd.execute(['job', 'status', 'west/bozo/test/hello'])
+ assert result == EXIT_API_ERROR
+ mock_scheduler_client.getTasksStatus.assert_call_count == 1