diff --git a/doc/source/server.rst b/doc/source/server.rst index 64b19b828..933829f3a 100644 --- a/doc/source/server.rst +++ b/doc/source/server.rst @@ -12,3 +12,7 @@ Server .. autoclass:: MessageHandlingServer :members: + +.. autofunction:: expected_exceptions + +.. autoexception:: ExpectedException diff --git a/oslo/messaging/_drivers/amqpdriver.py b/oslo/messaging/_drivers/amqpdriver.py index ab77f4f78..3486a53a6 100644 --- a/oslo/messaging/_drivers/amqpdriver.py +++ b/oslo/messaging/_drivers/amqpdriver.py @@ -37,7 +37,8 @@ class AMQPIncomingMessage(base.IncomingMessage): self.msg_id = msg_id self.reply_q = reply_q - def _send_reply(self, conn, reply=None, failure=None, ending=False): + def _send_reply(self, conn, reply=None, failure=None, + ending=False, log_failure=True): if failure: failure = rpc_common.serialize_remote_exception(failure) @@ -56,9 +57,9 @@ class AMQPIncomingMessage(base.IncomingMessage): else: conn.direct_send(self.msg_id, rpc_common.serialize_msg(msg)) - def reply(self, reply=None, failure=None): + def reply(self, reply=None, failure=None, log_failure=True): with self.listener.driver._get_connection() as conn: - self._send_reply(conn, reply, failure) + self._send_reply(conn, reply, failure, log_failure=log_failure) self._send_reply(conn, ending=True) diff --git a/oslo/messaging/_drivers/base.py b/oslo/messaging/_drivers/base.py index 1dd49f4e6..0868d6d1d 100644 --- a/oslo/messaging/_drivers/base.py +++ b/oslo/messaging/_drivers/base.py @@ -33,7 +33,7 @@ class IncomingMessage(object): self.message = message @abc.abstractmethod - def reply(self, reply=None, failure=None): + def reply(self, reply=None, failure=None, log_failure=True): "Send a reply or failure back to the client." diff --git a/oslo/messaging/_drivers/impl_fake.py b/oslo/messaging/_drivers/impl_fake.py index 3b4c99e18..7b9ab6d57 100644 --- a/oslo/messaging/_drivers/impl_fake.py +++ b/oslo/messaging/_drivers/impl_fake.py @@ -31,7 +31,7 @@ class FakeIncomingMessage(base.IncomingMessage): super(FakeIncomingMessage, self).__init__(listener, ctxt, message) self._reply_q = reply_q - def reply(self, reply=None, failure=None): + def reply(self, reply=None, failure=None, log_failure=True): # FIXME: handle failure if self._reply_q: self._reply_q.put(reply) diff --git a/oslo/messaging/_executors/base.py b/oslo/messaging/_executors/base.py index 62101a759..7a58bb4f4 100644 --- a/oslo/messaging/_executors/base.py +++ b/oslo/messaging/_executors/base.py @@ -16,6 +16,8 @@ import abc import logging import sys +from oslo import messaging + _LOG = logging.getLogger(__name__) @@ -33,6 +35,10 @@ class ExecutorBase(object): reply = self.callback(incoming.ctxt, incoming.message) if reply: incoming.reply(reply) + except messaging.ExpectedException as e: + _LOG.debug('Expected exception during message handling (%s)' % + e.exc_info[1]) + incoming.reply(failure=e.exc_info, log_failure=False) except Exception: # sys.exc_info() is deleted by LOG.exception(). exc_info = sys.exc_info() diff --git a/oslo/messaging/rpc/__init__.py b/oslo/messaging/rpc/__init__.py index c69e4b083..69d0ebd87 100644 --- a/oslo/messaging/rpc/__init__.py +++ b/oslo/messaging/rpc/__init__.py @@ -15,12 +15,14 @@ __all__ = [ 'ClientSendError', + 'ExpectedException', 'NoSuchMethod', 'RPCClient', 'RPCDispatcher', 'RPCDispatcherError', 'RPCVersionCapError', 'UnsupportedVersion', + 'expected_exceptions', 'get_rpc_server', ] diff --git a/oslo/messaging/rpc/server.py b/oslo/messaging/rpc/server.py index 943400e43..8c86ce448 100644 --- a/oslo/messaging/rpc/server.py +++ b/oslo/messaging/rpc/server.py @@ -89,7 +89,13 @@ return values from the methods. By supplying a serializer object, a server can deserialize arguments from - serialize return values to - primitive types. """ -__all__ = ['get_rpc_server'] +__all__ = [ + 'get_rpc_server', + 'ExpectedException', + 'expected_exceptions', +] + +import sys from oslo.messaging.rpc import dispatcher as rpc_dispatcher from oslo.messaging import server as msg_server @@ -117,3 +123,38 @@ def get_rpc_server(transport, target, endpoints, dispatcher = rpc_dispatcher.RPCDispatcher(endpoints, serializer) return msg_server.MessageHandlingServer(transport, target, dispatcher, executor) + + +class ExpectedException(Exception): + """Encapsulates an expected exception raised by an RPC endpoint + + Merely instantiating this exception records the current exception + information, which will be passed back to the RPC client without + exceptional logging. + """ + def __init__(self): + self.exc_info = sys.exc_info() + + +def expected_exceptions(*exceptions): + """Decorator for RPC endpoint methods that raise expected exceptions. + + Marking an endpoint method with this decorator allows the declaration + of expected exceptions that the RPC server should not consider fatal, + and not log as if they were generated in a real error scenario. + + Note that this will cause listed exceptions to be wrapped in an + ExpectedException, which is used internally by the RPC sever. The RPC + client will see the original exception type. + """ + def outer(func): + def inner(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception as e: + if type(e) in exceptions: + raise ExpectedException() + else: + raise + return inner + return outer diff --git a/tests/test_expected_exceptions.py b/tests/test_expected_exceptions.py new file mode 100644 index 000000000..1585e4b0d --- /dev/null +++ b/tests/test_expected_exceptions.py @@ -0,0 +1,54 @@ + +# Copyright 2012 OpenStack Foundation +# Copyright 2013 Red Hat, Inc. +# +# 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. + +from oslo import messaging + +from tests import utils as test_utils + + +class TestExpectedExceptions(test_utils.BaseTestCase): + + def test_exception(self): + e = None + try: + try: + raise ValueError() + except Exception: + raise messaging.ExpectedException() + except messaging.ExpectedException as e: + self.assertIsInstance(e, messaging.ExpectedException) + self.assertTrue(hasattr(e, 'exc_info')) + self.assertIsInstance(e.exc_info[1], ValueError) + + def test_decorator_expected(self): + class FooException(Exception): + pass + + @messaging.expected_exceptions(FooException) + def naughty(): + raise FooException() + + self.assertRaises(messaging.ExpectedException, naughty) + + def test_decorator_unexpected(self): + class FooException(Exception): + pass + + @messaging.expected_exceptions(FooException) + def really_naughty(): + raise ValueError() + + self.assertRaises(ValueError, really_naughty) diff --git a/tests/test_rabbit.py b/tests/test_rabbit.py index 4f845b8be..31c03f566 100644 --- a/tests/test_rabbit.py +++ b/tests/test_rabbit.py @@ -60,7 +60,8 @@ class TestSendReceive(test_utils.BaseTestCase): _failure = [ ('success', dict(failure=False)), - ('failure', dict(failure=True)), + ('failure', dict(failure=True, expected=False)), + ('expected_failure', dict(failure=True, expected=True)), ] _timeout = [ @@ -134,7 +135,8 @@ class TestSendReceive(test_utils.BaseTestCase): raise ZeroDivisionError except Exception: failure = sys.exc_info() - msgs[i].reply(failure=failure) + msgs[i].reply(failure=failure, + log_failure=not self.expected) else: msgs[i].reply({'bar': msgs[i].message['foo']}) senders[i].join()