Ejemplo n.º 1
0
class InProgress(Signal, Object):
    """
    InProgress objects are returned from functions that require more time to
    complete (because they are either blocked on some resource, are executing
    in a thread, or perhaps simply because they yielded control back to the main
    loop as a form of cooperative time slicing).

    InProgress subclasses :class:`~kaa.Signal`, which means InProgress objects are
    themselves signals.  Callbacks connected to an InProgress receive a single
    argument containing the result of the asynchronously executed task.

    If the asynchronous task raises an exception, the
    :attr:`~kaa.InProgress.exception` member, which is a separate signal, is
    emitted instead.
    """
    __kaasignals__ = {
        'abort':
            '''
            Emitted when abort() is called.

            .. describe:: def callback(exc)

               :param exc: an exception object the InProgress was aborted with.
               :type exc: InProgressAborted

            If the task cannot be aborted, the callback can return False, which
            will cause an exception to be raised by abort().
            '''
    }


    def __init__(self, abortable=False, frame=0):
        """
        :param abortable: see the :attr:`~kaa.InProgress.abortable` property.  
                          (Default: False)
        :type abortable: bool
        """
        super(InProgress, self).__init__()
        self._exception_signal = Signal()
        self._finished = False
        self._finished_event = threading.Event()
        self._exception = None
        self._unhandled_exception = None
        # TODO: make progress a property so we can document it.
        self.progress = None
        self.abortable = abortable

        # Stack frame for the caller who is creating us, for debugging.
        self._stack = traceback.extract_stack()[:frame-2]
        self._name = 'owner=%s:%d:%s()' % self._stack[-1][:3]


    def __repr__(self):
        return '<%s object at 0x%08x, %s>' % (self.__class__.__name__, id(self), self._name)


    def __inprogress__(self):
        """
        We subclass Signal which implements this method, but as we are already
        an InProgress, we simply return self.
        """
        return self

    @property
    def exception(self):
        """
        A :class:`~kaa.Signal` emitted when the asynchronous task this InProgress
        represents has raised an exception.

        Callbacks connected to this signal receive three arguments: exception class,
        exception instance, traceback.
        """
        return self._exception_signal


    @property
    def finished(self):
        """
        True if the InProgress is finished.
        """
        return self._finished


    @property
    def result(self):
        """
        The result the InProgress was finished with.  If an exception was thrown
        to the InProgress, accessing this property will raise that exception.
        """
        if not self._finished:
            raise RuntimeError('operation not finished')
        if self._exception:
            self._unhandled_exception = None
            if self._exception[2]:
                # We have the traceback, so we can raise using it.
                exc_type, exc_value, exc_tb_or_stack = self._exception
                raise exc_type, exc_value, exc_tb_or_stack
            else:
                # No traceback, so construct an AsyncException based on the
                # stack.
                raise self._exception[1]

        return self._result


    @property
    def failed(self):
        """
        True if an exception was thrown to the InProgress, False if it was
        finished without error or if it is not yet finished.
        """
        return bool(self._exception)


    # XXX: is this property sane after all?  Maybe there is no such case where
    # an IP can be just ignored upon abort().  kaa.delay() turned out not to be
    # an example after all.
    @property
    def abortable(self):
        """
        True if the asynchronous task this InProgress represents can be
        aborted by a call to :meth:`~kaa.InProgress.abort`.

        Normally :meth:`~kaa.InProgress.abort` will fail if there are no
        callbacks attached to the :attr:`~kaa.InProgress.signals.abort` signal.
        This property may be explicitly set to ``True``, in which case
        :meth:`~kaa.InProgress.abort` will succeed regardless.  An InProgress is
        therefore abortable if the ``abortable`` property has been explicitly
        set to True, if if there are callbacks connected to the
        :attr:`~kaa.InProgress.signals.abort` signal.

        This is useful when constructing an InProgress object that corresponds
        to an asynchronous task that can be safely aborted with no explicit action.
        """
        return self._abortable or self.signals['abort'].count() > 0


    @abortable.setter
    def abortable(self, abortable):
        self._abortable = abortable


    def finish(self, result):
        """
        This method should be called when the owner (creator) of the InProgress is
        finished successfully (with no exception).

        Any callbacks connected to the InProgress will then be emitted with the
        result passed to this method.

        If *result* is an unfinished InProgress, then instead of finishing, we
        wait for the result to finish.

        :param result: the result of the completed asynchronous task.  (This can
                       be thought of as the return value of the task if it had
                       been executed synchronously.)
        :return: This method returns self, which makes it convenient to prime InProgress
                 objects with a finished value. e.g. ``return InProgress().finish(42)``
        """
        if self._finished:
            raise RuntimeError('%s already finished' % self)
        if isinstance(result, InProgress) and result is not self:
            # we are still not finished, wait for this new InProgress
            self.waitfor(result)
            return self

        # store result
        self._finished = True
        self._result = result
        self._exception = None
        # Wake any threads waiting on us
        self._finished_event.set()
        # emit signal
        self.emit_when_handled(result)
        # cleanup
        self.disconnect_all()
        self._exception_signal.disconnect_all()
        self.signals['abort'].disconnect_all()
        return self


    def throw(self, type, value, tb, aborted=False):
        """
        This method should be called when the owner (creator) of the InProgress is
        finished because it raised an exception.

        Any callbacks connected to the :attr:`~kaa.InProgress.exception` signal will
        then be emitted with the arguments passed to this method.

        The parameters correspond to sys.exc_info().

        :param type: the class of the exception
        :param value: the instance of the exception
        :param tb: the traceback object representing where the exception took place
        """
        # This function must deal with a tricky problem.  See:
        # http://mail.python.org/pipermail/python-dev/2005-September/056091.html
        #
        # Ideally, we want to store the traceback object so we can defer the
        # exception handling until some later time.  The problem is that by
        # storing the traceback, we create some ridiculously deep circular
        # references.
        #
        # The way we deal with this is to pass along the traceback object to
        # any handler that can handle the exception immediately, and then
        # discard the traceback.  A stringified formatted traceback is attached
        # to the exception in the formatted_traceback attribute.
        #
        # The above URL suggests a possible non-trivial workaround: create a
        # custom traceback object in C code that preserves the parts of the
        # stack frames needed for printing tracebacks, but discarding objects
        # that would create circular references.  This might be a TODO.

        self._finished = True
        self._exception = type, value, tb
        self._unhandled_exception = True
        stack = traceback.extract_tb(tb)

        # Attach a stringified traceback to the exception object.  Right now,
        # this is the best we can do for asynchronous handlers.
        trace = ''.join(traceback.format_exception(*self._exception)).strip()
        value.formatted_traceback = trace

        # Wake any threads waiting on us.  We've initialized _exception with
        # the traceback object, so any threads that access the result property
        # between now and the end of this function will have an opportunity to
        # get the live traceback.
        self._finished_event.set()

        if self._exception_signal.count() == 0:
            # There are no exception handlers, so we know we will end up
            # queuing the traceback in the exception signal.  Set it to None
            # to prevent that.
            tb = None

        if self._exception_signal.emit_when_handled(type, value, tb) == False:
            # A handler has acknowledged handling this exception by returning
            # False.  So we won't log it.
            self._unhandled_exception = None

        if isinstance(value, InProgressAborted):
            if not aborted:
                # An InProgress we were waiting on has been aborted, so we
                # abort too.
                self.signals['abort'].emit(value)
            self._unhandled_exception = None

        if self._unhandled_exception:
            # This exception was not handled synchronously, so we set up a
            # weakref object with a finalize callback to a function that
            # logs the exception.  We could do this in __del__, except that
            # the gc refuses to collect objects with a destructor.  The weakref
            # kludge lets us accomplish the same thing without actually using
            # __del__.
            #
            # If the exception is passed back via result property, then it is
            # considered handled, and it will not be logged.
            cb = Callback(InProgress._log_exception, trace, value, self._stack)
            self._unhandled_exception = _weakref.ref(self, cb)

        # Remove traceback from stored exception.  If any waiting threads
        # haven't gotten it by now, it's too late.
        if not isinstance(value, AsyncExceptionBase):
            value = AsyncException(value, stack)
        self._exception = value.__class__, value, None

        # cleanup
        self.disconnect_all()
        self._exception_signal.disconnect_all()
        self.signals['abort'].disconnect_all()

        # We return False here so that if we've received a thrown exception
        # from another InProgress we're waiting on, we essentially inherit
        # the exception from it and indicate to it that we'll handle it
        # from here on.  (Otherwise the linked InProgress would figure
        # nobody handled it and would dump out an unhandled async exception.)
        return False

    @classmethod
    def _log_exception(cls, weakref, trace, exc, create_stack):
        """
        Callback to log unhandled exceptions.
        """
        if isinstance(exc, (KeyboardInterrupt, SystemExit)):
            # We have an unhandled asynchronous SystemExit or KeyboardInterrupt
            # exception.  Rather than logging it, we reraise it in the main
            # loop so that the main loop exception handler can act
            # appropriately.
            def reraise():
                raise exc
            return main.signals['step'].connect_once(reraise)

        log.error('Unhandled %s exception:\n%s', cls.__name__, trace)
        if log.level <= logging.INFO:
            # Asynchronous exceptions create a bit of a problem in that while you
            # know where the exception came from, you don't easily know where it
            # was going.  Here we dump the stack obtained in the constructor,
            # so it's possible to find out which caller didn't properly catch
            # the exception.
            create_tb = ''.join(traceback.format_list(create_stack))
            log.info('Create-stack for InProgress from preceding exception:\n%s', create_tb)


    def abort(self, exc=None):
        """
        Aborts the asynchronous task this InProgress represents.

        :param exc: optional exception object with which to abort the InProgress; if
                    None is given, a general InProgressAborted exception will
                    be used.
        :type exc: InProgressAborted

        Not all such tasks can be aborted.  If aborting is not supported, or if
        the InProgress is already finished, a RuntimeError exception is raised.

        If a coroutine is aborted, the CoroutineInProgress object returned by
        the coroutine will be finished with InProgressAborted, while the underlying
        generator used by the coroutine will have the standard GeneratorExit
        raised inside it.
        """
        if self.finished:
            raise RuntimeError('InProgress is already finished.')

        if exc is None:
            exc = InProgressAborted('InProgress task aborted by abort()')
        elif not isinstance(exc, InProgressAborted):
            raise ValueError('Exception must be instance of InProgressAborted (or subclass thereof)')

        if not self.abortable or self.signals['abort'].emit(exc) == False:
            raise RuntimeError('%s cannot be aborted.' % self)

        self.throw(exc.__class__, exc, None, aborted=True)


    def timeout(self, timeout, callback=None, abort=False):
        """
        Create a new InProgress object linked to this one that will throw
        a TimeoutException if this object is not finished by the given timeout.

        :param callback: called (with no additional arguments) just prior
                         to TimeoutException
        :return: a new :class:`~kaa.InProgress` object that is subject to the timeout

        If the original InProgress finishes before the timeout, the new InProgress
        (returned by this method) is finished with the result of the original.

        If a timeout does occur, the original InProgress object is not affected:
        it is not finished with the TimeoutException, nor is it aborted.  If you
        want to abort the original task you must do it explicitly::

            @kaa.coroutine()
            def read_from_socket(sock):
                try:
                    data = yield sock.read().timeout(3)
                except TimeoutException, (msg, inprogress):
                    print 'Error:', msg
                    inprogress.abort()
        """
        async = InProgress()
        def trigger():
            self.disconnect(async.finish)
            self._exception_signal.disconnect(async.throw)
            if not async._finished:
                if callback:
                    callback()
                msg = 'InProgress timed out after %.02f seconds' % timeout
                async.throw(TimeoutException, TimeoutException(msg, self), None)
                if abort:
                    self.abort()
        async.waitfor(self)
        OneShotTimer(trigger).start(timeout)
        return async