 0d884a2fc5
			
		
	
	0d884a2fc5
	
	
	
		
			
			The oslo.utils library now provides a better version of this that always returns a unicode exception message, so update our usage to use it (and remove our own local function). This guarantee of unicode also means we have to update a few other places to make sure we get back bytes or unicode as needed. Change-Id: I924380408aaf6d2aec418ceaaf623c75900268f7
		
			
				
	
	
		
			303 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			303 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # -*- coding: utf-8 -*-
 | |
| 
 | |
| #    Copyright (C) 2012 Yahoo! Inc. All Rights Reserved.
 | |
| #
 | |
| #    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 os
 | |
| import traceback
 | |
| 
 | |
| from oslo_utils import excutils
 | |
| from oslo_utils import reflection
 | |
| import six
 | |
| from taskflow.utils import mixins
 | |
| 
 | |
| 
 | |
| def raise_with_cause(exc_cls, message, *args, **kwargs):
 | |
|     """Helper to raise + chain exceptions (when able) and associate a *cause*.
 | |
| 
 | |
|     NOTE(harlowja): Since in py3.x exceptions can be chained (due to
 | |
|     :pep:`3134`) we should try to raise the desired exception with the given
 | |
|     *cause* (or extract a *cause* from the current stack if able) so that the
 | |
|     exception formats nicely in old and new versions of python. Since py2.x
 | |
|     does **not** support exception chaining (or formatting) our root exception
 | |
|     class has a :py:meth:`~taskflow.exceptions.TaskFlowException.pformat`
 | |
|     method that can be used to get *similar* information instead (and this
 | |
|     function makes sure to retain the *cause* in that case as well so
 | |
|     that the :py:meth:`~taskflow.exceptions.TaskFlowException.pformat` method
 | |
|     shows them).
 | |
| 
 | |
|     :param exc_cls: the :py:class:`~taskflow.exceptions.TaskFlowException`
 | |
|                     class to raise.
 | |
|     :param message: the text/str message that will be passed to
 | |
|                     the exceptions constructor as its first positional
 | |
|                     argument.
 | |
|     :param args: any additional positional arguments to pass to the
 | |
|                  exceptions constructor.
 | |
|     :param kwargs: any additional keyword arguments to pass to the
 | |
|                    exceptions constructor.
 | |
|     """
 | |
|     if not issubclass(exc_cls, TaskFlowException):
 | |
|         raise ValueError("Subclass of taskflow exception is required")
 | |
|     excutils.raise_with_cause(exc_cls, message, *args, **kwargs)
 | |
| 
 | |
| 
 | |
| class TaskFlowException(Exception):
 | |
|     """Base class for *most* exceptions emitted from this library.
 | |
| 
 | |
|     NOTE(harlowja): in later versions of python we can likely remove the need
 | |
|     to have a ``cause`` here as PY3+ have implemented :pep:`3134` which
 | |
|     handles chaining in a much more elegant manner.
 | |
| 
 | |
|     :param message: the exception message, typically some string that is
 | |
|                     useful for consumers to view when debugging or analyzing
 | |
|                     failures.
 | |
|     :param cause: the cause of the exception being raised, when provided this
 | |
|                   should itself be an exception instance, this is useful for
 | |
|                   creating a chain of exceptions for versions of python where
 | |
|                   this is not yet implemented/supported natively.
 | |
|     """
 | |
|     def __init__(self, message, cause=None):
 | |
|         super(TaskFlowException, self).__init__(message)
 | |
|         self._cause = cause
 | |
| 
 | |
|     @property
 | |
|     def cause(self):
 | |
|         return self._cause
 | |
| 
 | |
|     def __str__(self):
 | |
|         return self.pformat()
 | |
| 
 | |
|     def _get_message(self):
 | |
|         # We must *not* call into the __str__ method as that will reactivate
 | |
|         # the pformat method, which will end up badly (and doesn't look
 | |
|         # pretty at all); so be careful...
 | |
|         return self.args[0]
 | |
| 
 | |
|     def pformat(self, indent=2, indent_text=" ", show_root_class=False):
 | |
|         """Pretty formats a taskflow exception + any connected causes."""
 | |
|         if indent < 0:
 | |
|             raise ValueError("Provided 'indent' must be greater than"
 | |
|                              " or equal to zero instead of %s" % indent)
 | |
|         buf = six.StringIO()
 | |
|         if show_root_class:
 | |
|             buf.write(reflection.get_class_name(self, fully_qualified=False))
 | |
|             buf.write(": ")
 | |
|         buf.write(self._get_message())
 | |
|         active_indent = indent
 | |
|         next_up = self.cause
 | |
|         seen = []
 | |
|         while next_up is not None and next_up not in seen:
 | |
|             seen.append(next_up)
 | |
|             buf.write(os.linesep)
 | |
|             if isinstance(next_up, TaskFlowException):
 | |
|                 buf.write(indent_text * active_indent)
 | |
|                 buf.write(reflection.get_class_name(next_up,
 | |
|                                                     fully_qualified=False))
 | |
|                 buf.write(": ")
 | |
|                 buf.write(next_up._get_message())
 | |
|             else:
 | |
|                 lines = traceback.format_exception_only(type(next_up), next_up)
 | |
|                 for i, line in enumerate(lines):
 | |
|                     buf.write(indent_text * active_indent)
 | |
|                     if line.endswith("\n"):
 | |
|                         # We'll add our own newlines on...
 | |
|                         line = line[0:-1]
 | |
|                     buf.write(line)
 | |
|                     if i + 1 != len(lines):
 | |
|                         buf.write(os.linesep)
 | |
|             if not isinstance(next_up, TaskFlowException):
 | |
|                 # Don't go deeper into non-taskflow exceptions... as we
 | |
|                 # don't know if there exception 'cause' attributes are even
 | |
|                 # useable objects...
 | |
|                 break
 | |
|             active_indent += indent
 | |
|             next_up = getattr(next_up, 'cause', None)
 | |
|         return buf.getvalue()
 | |
| 
 | |
| 
 | |
| # Errors related to storage or operations on storage units.
 | |
| 
 | |
| class StorageFailure(TaskFlowException):
 | |
|     """Raised when storage backends can not be read/saved/deleted."""
 | |
| 
 | |
| 
 | |
| # Conductor related errors.
 | |
| 
 | |
| class ConductorFailure(TaskFlowException):
 | |
|     """Errors related to conducting activities."""
 | |
| 
 | |
| 
 | |
| # Job related errors.
 | |
| 
 | |
| class JobFailure(TaskFlowException):
 | |
|     """Errors related to jobs or operations on jobs."""
 | |
| 
 | |
| 
 | |
| class UnclaimableJob(JobFailure):
 | |
|     """Raised when a job can not be claimed."""
 | |
| 
 | |
| 
 | |
| # Engine/ during execution related errors.
 | |
| 
 | |
| class ExecutionFailure(TaskFlowException):
 | |
|     """Errors related to engine execution."""
 | |
| 
 | |
| 
 | |
| class RequestTimeout(ExecutionFailure):
 | |
|     """Raised when a worker request was not finished within allotted time."""
 | |
| 
 | |
| 
 | |
| class InvalidState(ExecutionFailure):
 | |
|     """Raised when a invalid state transition is attempted while executing."""
 | |
| 
 | |
| 
 | |
| # Other errors that do not fit the above categories (at the current time).
 | |
| 
 | |
| 
 | |
| class DependencyFailure(TaskFlowException):
 | |
|     """Raised when some type of dependency problem occurs."""
 | |
| 
 | |
| 
 | |
| class AmbiguousDependency(DependencyFailure):
 | |
|     """Raised when some type of ambiguous dependency problem occurs."""
 | |
| 
 | |
| 
 | |
| class MissingDependencies(DependencyFailure):
 | |
|     """Raised when a entity has dependencies that can not be satisfied.
 | |
| 
 | |
|     :param who: the entity that caused the missing dependency to be triggered.
 | |
|     :param requirements: the dependency which were not satisfied.
 | |
| 
 | |
|     Further arguments are interpreted as for in
 | |
|     :py:class:`~taskflow.exceptions.TaskFlowException`.
 | |
|     """
 | |
| 
 | |
|     #: Exception message template used when creating an actual message.
 | |
|     MESSAGE_TPL = ("'%(who)s' requires %(requirements)s but no other entity"
 | |
|                    " produces said requirements")
 | |
| 
 | |
|     def __init__(self, who, requirements, cause=None):
 | |
|         message = self.MESSAGE_TPL % {'who': who, 'requirements': requirements}
 | |
|         super(MissingDependencies, self).__init__(message, cause=cause)
 | |
|         self.missing_requirements = requirements
 | |
| 
 | |
| 
 | |
| class CompilationFailure(TaskFlowException):
 | |
|     """Raised when some type of compilation issue is found."""
 | |
| 
 | |
| 
 | |
| class IncompatibleVersion(TaskFlowException):
 | |
|     """Raised when some type of version incompatibility is found."""
 | |
| 
 | |
| 
 | |
| class Duplicate(TaskFlowException):
 | |
|     """Raised when a duplicate entry is found."""
 | |
| 
 | |
| 
 | |
| class NotFound(TaskFlowException):
 | |
|     """Raised when some entry in some object doesn't exist."""
 | |
| 
 | |
| 
 | |
| class Empty(TaskFlowException):
 | |
|     """Raised when some object is empty when it shouldn't be."""
 | |
| 
 | |
| 
 | |
| class MultipleChoices(TaskFlowException):
 | |
|     """Raised when some decision can't be made due to many possible choices."""
 | |
| 
 | |
| 
 | |
| class InvalidFormat(TaskFlowException):
 | |
|     """Raised when some object/entity is not in the expected format."""
 | |
| 
 | |
| 
 | |
| # Others.
 | |
| 
 | |
| class NotImplementedError(NotImplementedError):
 | |
|     """Exception for when some functionality really isn't implemented.
 | |
| 
 | |
|     This is typically useful when the library itself needs to distinguish
 | |
|     internal features not being made available from users features not being
 | |
|     made available/implemented (and to avoid misinterpreting the two).
 | |
|     """
 | |
| 
 | |
| 
 | |
| class WrappedFailure(mixins.StrMixin, Exception):
 | |
|     """Wraps one or several failure objects.
 | |
| 
 | |
|     When exception/s cannot be re-raised (for example, because the value and
 | |
|     traceback are lost in serialization) or there are several exceptions active
 | |
|     at the same time (due to more than one thread raising exceptions), we will
 | |
|     wrap the corresponding failure objects into this exception class and
 | |
|     *may* reraise this exception type to allow users to handle the contained
 | |
|     failures/causes as they see fit...
 | |
| 
 | |
|     See the failure class documentation for a more comprehensive set of reasons
 | |
|     why this object *may* be reraised instead of the original exception.
 | |
| 
 | |
|     :param causes: the :py:class:`~taskflow.types.failure.Failure` objects
 | |
|                    that caused this this exception to be raised.
 | |
|     """
 | |
| 
 | |
|     def __init__(self, causes):
 | |
|         super(WrappedFailure, self).__init__()
 | |
|         self._causes = []
 | |
|         for cause in causes:
 | |
|             if cause.check(type(self)) and cause.exception:
 | |
|                 # NOTE(imelnikov): flatten wrapped failures.
 | |
|                 self._causes.extend(cause.exception)
 | |
|             else:
 | |
|                 self._causes.append(cause)
 | |
| 
 | |
|     def __iter__(self):
 | |
|         """Iterate over failures that caused the exception."""
 | |
|         return iter(self._causes)
 | |
| 
 | |
|     def __len__(self):
 | |
|         """Return number of wrapped failures."""
 | |
|         return len(self._causes)
 | |
| 
 | |
|     def check(self, *exc_classes):
 | |
|         """Check if any of exception classes caused the failure/s.
 | |
| 
 | |
|         :param exc_classes: exception types/exception type names to
 | |
|                             search for.
 | |
| 
 | |
|         If any of the contained failures were caused by an exception of a
 | |
|         given type, the corresponding argument that matched is returned. If
 | |
|         not then none is returned.
 | |
|         """
 | |
|         if not exc_classes:
 | |
|             return None
 | |
|         for cause in self:
 | |
|             result = cause.check(*exc_classes)
 | |
|             if result is not None:
 | |
|                 return result
 | |
|         return None
 | |
| 
 | |
|     def __bytes__(self):
 | |
|         buf = six.BytesIO()
 | |
|         buf.write(b'WrappedFailure: [')
 | |
|         causes_gen = (six.binary_type(cause) for cause in self._causes)
 | |
|         buf.write(b", ".join(causes_gen))
 | |
|         buf.write(b']')
 | |
|         return buf.getvalue()
 | |
| 
 | |
|     def __unicode__(self):
 | |
|         buf = six.StringIO()
 | |
|         buf.write(u'WrappedFailure: [')
 | |
|         causes_gen = (six.text_type(cause) for cause in self._causes)
 | |
|         buf.write(u", ".join(causes_gen))
 | |
|         buf.write(u']')
 | |
|         return buf.getvalue()
 |